@camstack/addon-export-hap 1.2.27 → 1.2.29

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.
@@ -84,7 +84,7 @@ function carryForward(base, existing, keys) {
84
84
  return out;
85
85
  }
86
86
  //#endregion
87
- //#region ../types/dist/event-category-Cv9dO26A.mjs
87
+ //#region ../types/dist/event-category-Bxo5yJjt.mjs
88
88
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
89
89
  EventCategory["SystemBoot"] = "system.boot";
90
90
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -291,6 +291,33 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
291
291
  EventCategory["PipelineCameraAssigned"] = "pipeline.camera-assigned";
292
292
  EventCategory["PipelineCameraUnassigned"] = "pipeline.camera-unassigned";
293
293
  /**
294
+ * A node the orchestrator would otherwise place cameras on has NO usable
295
+ * inference device: the operator enabled one or more accelerators there and
296
+ * the live probe reports every one of them unavailable. Emitted once per
297
+ * TRANSITION into that state (never per dispatch), and the node is dropped
298
+ * from the placement candidate set for as long as it holds.
299
+ *
300
+ * This exists because the state was previously invisible: little-unraid
301
+ * absorbed 283k inference errors in a day while still being handed cameras,
302
+ * and nothing in the system said so.
303
+ *
304
+ * A node with no accelerators configured at all is NOT this — its devices
305
+ * are `disabled`, not `unavailable`, and the runner's default CPU pool
306
+ * serves it exactly as before.
307
+ */
308
+ EventCategory["PipelineNodeInferenceUnavailable"] = "pipeline.node-inference-unavailable";
309
+ /**
310
+ * A camera has an OPEN detection session and has produced no detection at
311
+ * all for longer than the blind threshold — the camera is being decoded and
312
+ * inferred and is returning nothing. Emitted once per transition into blind,
313
+ * per camera.
314
+ *
315
+ * The failure it reports: a 1h43 detection blackout on the entrance camera
316
+ * that nobody noticed, because "a camera that detects nothing" and "a quiet
317
+ * camera" produce byte-identical silence.
318
+ */
319
+ EventCategory["PipelineDetectionBlind"] = "pipeline.detection-blind";
320
+ /**
294
321
  * Per-camera pipeline config was mutated by the orchestrator
295
322
  * (3-level settings change via `setAgentAddonDefaults` /
296
323
  * `setCameraStepToggle` / `setCameraPipelineForAgent` or a
@@ -3082,6 +3109,9 @@ function handlePipeResult(left, next, ctx) {
3082
3109
  fallback: left.fallback
3083
3110
  }, ctx);
3084
3111
  }
3112
+ var $ZodPreprocess = /*@__PURE__*/ $constructor("$ZodPreprocess", (inst, def) => {
3113
+ $ZodPipe.init(inst, def);
3114
+ });
3085
3115
  var $ZodReadonly = /*@__PURE__*/ $constructor("$ZodReadonly", (inst, def) => {
3086
3116
  $ZodType.init(inst, def);
3087
3117
  defineLazy(inst._zod, "propValues", () => def.innerType._zod.propValues);
@@ -5267,6 +5297,10 @@ function pipe(in_, out) {
5267
5297
  out
5268
5298
  });
5269
5299
  }
5300
+ var ZodPreprocess = /*@__PURE__*/ $constructor("ZodPreprocess", (inst, def) => {
5301
+ ZodPipe.init(inst, def);
5302
+ $ZodPreprocess.init(inst, def);
5303
+ });
5270
5304
  var ZodReadonly = /*@__PURE__*/ $constructor("ZodReadonly", (inst, def) => {
5271
5305
  $ZodReadonly.init(inst, def);
5272
5306
  ZodType.init(inst, def);
@@ -5325,6 +5359,13 @@ function _instanceof(cls, params = {}) {
5325
5359
  };
5326
5360
  return inst;
5327
5361
  }
5362
+ function preprocess(fn, schema) {
5363
+ return new ZodPreprocess({
5364
+ type: "pipe",
5365
+ in: transform(fn),
5366
+ out: schema
5367
+ });
5368
+ }
5328
5369
  //#endregion
5329
5370
  //#region ../../node_modules/zod/v4/classic/compat.js
5330
5371
  /** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
@@ -7601,7 +7642,7 @@ import { errMsg } from '@camstack/types'
7601
7642
  * Extract a human-readable message from an unknown error value.
7602
7643
  * Replaces the ubiquitous `errMsg(err)` pattern.
7603
7644
  */
7604
- function errMsg$12(err) {
7645
+ function errMsg$15(err) {
7605
7646
  if (err instanceof Error) return err.message;
7606
7647
  if (typeof err === "string") return err;
7607
7648
  return String(err);
@@ -11494,6 +11535,8 @@ var QueryFilterSchema = object({
11494
11535
  where: record(string(), unknown()).optional(),
11495
11536
  whereIn: record(string(), array(unknown())).optional(),
11496
11537
  whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
11538
+ /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
11539
+ whereNot: record(string(), unknown()).optional(),
11497
11540
  orderBy: object({
11498
11541
  field: string(),
11499
11542
  direction: _enum(["asc", "desc"])
@@ -11513,7 +11556,8 @@ var QueryFilterSchema = object({
11513
11556
  var MutationFilterSchema = object({
11514
11557
  where: record(string(), unknown()).optional(),
11515
11558
  whereIn: record(string(), array(unknown())).optional(),
11516
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional()
11559
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
11560
+ whereNot: record(string(), unknown()).optional()
11517
11561
  });
11518
11562
  /** A single stored record: `{ id, data }`. */
11519
11563
  var SettingsRecordSchema = object({
@@ -12951,6 +12995,17 @@ var LlmImageSchema = object({
12951
12995
  bytes: _instanceof(Uint8Array),
12952
12996
  mimeType: string()
12953
12997
  });
12998
+ /**
12999
+ * Retry policy. `enabled: false` is NOT the same as `maxAttempts: 1` in intent —
13000
+ * the flag is what a consumer table flips, the count is what the operator tunes.
13001
+ * A retry doubles the wall time of a call, so the two gates that run inside a
13002
+ * notification's budget keep it off (see `CONSUMER_RETRY_POLICY` in addon-ai).
13003
+ */
13004
+ var LlmRetryPolicySchema = object({
13005
+ enabled: boolean().default(false),
13006
+ /** Total attempts INCLUDING the first. 1 = no retry. */
13007
+ maxAttempts: number().int().min(1).max(5).default(1)
13008
+ });
12954
13009
  var LlmGenerateBaseInputSchema = object({
12955
13010
  /** Collection routing (the notification-output posture). */
12956
13011
  addonId: string().optional(),
@@ -12965,7 +13020,28 @@ var LlmGenerateBaseInputSchema = object({
12965
13020
  jsonSchema: record(string(), unknown()).optional(),
12966
13021
  /** Per-call override of the profile default. */
12967
13022
  maxTokens: number().int().positive().optional(),
12968
- temperature: number().optional()
13023
+ temperature: number().optional(),
13024
+ /** Per-call override of the profile default (nucleus sampling). */
13025
+ topP: number().min(0).max(1).optional(),
13026
+ /** Per-call override of the profile default (top-k sampling). */
13027
+ topK: number().int().positive().optional(),
13028
+ /** Per-call override of `profile.timeoutMs` — the total generation bound. */
13029
+ timeoutMs: number().int().positive().optional(),
13030
+ /** Per-call override; beats both the consumer table and the profile. */
13031
+ retry: LlmRetryPolicySchema.optional(),
13032
+ /**
13033
+ * Caller-minted id that makes this generation CANCELLABLE.
13034
+ *
13035
+ * Without it a caller that stops waiting cannot stop the work: the gates race
13036
+ * the call against 8 s and free their own slot when the timer wins, while the
13037
+ * generation upstream keeps running to `profile.timeoutMs` — 60 s by default,
13038
+ * on a single-threaded local model. The per-camera bound then counts WAITS,
13039
+ * not generations, and the real load is unbounded.
13040
+ *
13041
+ * `AbortSignal` cannot cross a process boundary; an id can. Pass one here and
13042
+ * `llm.cancel({ requestId })` tears the socket down.
13043
+ */
13044
+ requestId: string().optional()
12969
13045
  });
12970
13046
  /**
12971
13047
  * `llm-runtime` — node-side managed llama.cpp executor (spec §4). Registered
@@ -12978,6 +13054,18 @@ var LlmGenerateBaseInputSchema = object({
12978
13054
  * Resource ceiling = llama-server flags + idleStopMinutes ONLY (no RSS
12979
13055
  * watchdog — operator decision #3).
12980
13056
  */
13057
+ /**
13058
+ * A companion artifact that MUST land beside the main GGUF: the `mmproj`
13059
+ * projector of a vision model, or shards 2..N of a split GGUF. Carried on the
13060
+ * REF rather than looked up at install time, so what the operator approved in
13061
+ * the preview is exactly what the node downloads.
13062
+ */
13063
+ var ManagedModelExtraFileSchema = object({
13064
+ url: string(),
13065
+ filename: string(),
13066
+ sizeBytes: number(),
13067
+ sha256: string().optional()
13068
+ });
12981
13069
  var ManagedModelRefSchema = discriminatedUnion("kind", [
12982
13070
  object({
12983
13071
  kind: literal("catalog"),
@@ -12986,7 +13074,11 @@ var ManagedModelRefSchema = discriminatedUnion("kind", [
12986
13074
  object({
12987
13075
  kind: literal("url"),
12988
13076
  url: string(),
12989
- sha256: string().optional()
13077
+ sha256: string().optional(),
13078
+ /** Picker/status label; the file basename when absent. */
13079
+ label: string().optional(),
13080
+ sizeBytes: number().optional(),
13081
+ extraFiles: array(ManagedModelExtraFileSchema).optional()
12990
13082
  }),
12991
13083
  object({
12992
13084
  kind: literal("path"),
@@ -13004,13 +13096,82 @@ var ManagedRuntimeConfigSchema = object({
13004
13096
  gpuLayers: number().int().default(0),
13005
13097
  /** Default: cpus-2, clamped ≥1 (resolved node-side). */
13006
13098
  threads: number().int().optional(),
13007
- /** Concurrent slots. */
13099
+ /** Concurrent slots (`--parallel`). */
13008
13100
  parallel: number().int().default(1),
13101
+ /** Logical batch size (`-b`). Larger = faster prompt ingest, more RAM. */
13102
+ batchSize: number().int().positive().optional(),
13103
+ /** Physical batch / micro-batch (`-ub`). */
13104
+ ubatchSize: number().int().positive().optional(),
13105
+ /**
13106
+ * `--flash-attn`. Cuts KV-cache memory on the backends that implement it and
13107
+ * is a no-op elsewhere, so it is offered rather than assumed.
13108
+ */
13109
+ flashAttention: boolean().default(false),
13110
+ /**
13111
+ * `--mlock`. Pins the weights in RAM so the OS cannot page them out mid
13112
+ * inference. Costs the full model size in resident memory — which is exactly
13113
+ * what the RAM budget is counting.
13114
+ */
13115
+ mlock: boolean().default(false),
13116
+ /**
13117
+ * `--no-mmap`. Reads the whole GGUF up front instead of mapping it. Slower to
13118
+ * start, but avoids the page-fault stalls a network or spinning-disk model
13119
+ * store produces on every first token.
13120
+ */
13121
+ noMmap: boolean().default(false),
13122
+ /** `--cache-type-k` / `--cache-type-v` — quantising the KV cache is the
13123
+ * cheapest way to fit a longer context in the same RAM. */
13124
+ cacheTypeK: _enum([
13125
+ "f32",
13126
+ "f16",
13127
+ "q8_0",
13128
+ "q5_1",
13129
+ "q5_0",
13130
+ "q4_1",
13131
+ "q4_0"
13132
+ ]).optional(),
13133
+ cacheTypeV: _enum([
13134
+ "f32",
13135
+ "f16",
13136
+ "q8_0",
13137
+ "q5_1",
13138
+ "q5_0",
13139
+ "q4_1",
13140
+ "q4_0"
13141
+ ]).optional(),
13142
+ /**
13143
+ * Escape hatch for llama-server flags this schema does NOT model — `--jinja`
13144
+ * (which most vision chat templates need and some language-only models
13145
+ * dislike), `--cont-batching`, `--rope-scaling`, …
13146
+ *
13147
+ * It is NOT a second place to set the flags above. A token that collides
13148
+ * with a typed field is REJECTED at start, naming the field that owns it
13149
+ * (`assertNoOwnedFlags`), because two knobs writing the same argv is exactly
13150
+ * the "two switches that disagree" failure this repo has already shipped
13151
+ * twice (D62).
13152
+ */
13153
+ extraArgs: array(string()).default([]),
13009
13154
  /** Else lazy: first generate boots it. */
13010
13155
  autoStart: boolean().default(false),
13011
13156
  /** 0 = never; frees RAM after quiet periods. */
13012
13157
  idleStopMinutes: number().int().default(30)
13013
13158
  });
13159
+ /**
13160
+ * Where a multi-GB install currently is. A single 0..1 fraction cannot answer
13161
+ * "is it stuck?" for an install that is three files (shards + mmproj) followed
13162
+ * by a sha256 pass over 22 GB — during which the fraction sat at 1.0 and the
13163
+ * node looked hung. Phase + file + bytes is the smallest shape that does.
13164
+ */
13165
+ var LlmDownloadProgressSchema = object({
13166
+ phase: _enum(["downloading", "verifying"]),
13167
+ /** The artifact currently moving, e.g. `mmproj-F16.gguf`. */
13168
+ file: string(),
13169
+ fileIndex: number().int(),
13170
+ fileCount: number().int(),
13171
+ /** Across the WHOLE install, not the current file. */
13172
+ downloadedBytes: number(),
13173
+ totalBytes: number().optional()
13174
+ });
13014
13175
  var LlmRuntimeStatusSchema = object({
13015
13176
  /** Status is ALWAYS node-qualified. */
13016
13177
  nodeId: string(),
@@ -13027,6 +13188,8 @@ var LlmRuntimeStatusSchema = object({
13027
13188
  modelPath: string().optional(),
13028
13189
  modelId: string().optional(),
13029
13190
  downloadProgress: number().min(0).max(1).optional(),
13191
+ /** Detail behind `downloadProgress`; present for the same lifetime. */
13192
+ download: LlmDownloadProgressSchema.optional(),
13030
13193
  lastError: string().optional(),
13031
13194
  crashesInWindow: number(),
13032
13195
  /** Child RSS (sampled best-effort). */
@@ -13037,7 +13200,14 @@ var LlmNodeModelSchema = object({
13037
13200
  file: string(),
13038
13201
  sizeBytes: number(),
13039
13202
  catalogId: string().optional(),
13040
- installedAt: number().optional()
13203
+ installedAt: number().optional(),
13204
+ /**
13205
+ * Absolute path on the node. Present so a file that is on disk but matches
13206
+ * no catalog entry — a custom Hugging Face install, or a GGUF the operator
13207
+ * copied in by hand — is still SELECTABLE, as a `{kind:'path'}` ref. Without
13208
+ * it the picker could list such a file and do nothing with it.
13209
+ */
13210
+ path: string().optional()
13041
13211
  });
13042
13212
  var LlmRuntimeDiskUsageSchema = object({
13043
13213
  nodeId: string(),
@@ -13093,10 +13263,47 @@ var LlmProfileSchema = object({
13093
13263
  baseUrl: string().optional(),
13094
13264
  /** ConfigUISchema type:'password' — never round-trips (spec §5). */
13095
13265
  apiKey: string().optional(),
13266
+ /** Vision on/off. A vision call against a `false` profile is REFUSED, never
13267
+ * degraded to text — that shipped once and produced a confident answer to a
13268
+ * question about a picture nobody sent. */
13096
13269
  supportsVision: boolean(),
13097
13270
  temperature: number().min(0).max(2).optional(),
13271
+ /** Nucleus sampling. Every wire we speak has it. */
13272
+ topP: number().min(0).max(1).optional(),
13273
+ /** Top-k sampling. Carried only by the wires that have it — NEITHER OpenAI
13274
+ * wire does, and the client drops it there (measured: the request body gets
13275
+ * `top_p` and no `top_k`). The profile editor hides the field wherever it
13276
+ * would change nothing; `KINDS_WITH_TOP_K` is the single owner of that list. */
13277
+ topK: number().int().positive().optional(),
13098
13278
  maxTokens: number().int().positive().optional(),
13279
+ /** Prompt context window. Advisory for cloud kinds (they enforce their own);
13280
+ * for `managed-local` it is the llama.cpp `--ctx-size` the runtime starts
13281
+ * the model with, so it is the one field that changes a PROCESS. */
13282
+ contextLength: number().int().positive().optional(),
13283
+ /** Default system prompt. A caller's `system` REPLACES it (never appends —
13284
+ * two system prompts fighting is worse than either alone). */
13285
+ systemPrompt: string().optional(),
13286
+ /** Total generation bound — the only one a unary call has. */
13099
13287
  timeoutMs: number().int().positive().default(6e4),
13288
+ /** The TCP handshake only — "is the port even open". NOT the wait for
13289
+ * response headers: on the LM Studio / llama-server wire those are written
13290
+ * once the model has finished loading, so they belong to the bound below. */
13291
+ connectTimeoutMs: number().int().positive().default(1e4),
13292
+ /** Accepted, but no output yet — response headers included, because a cold
13293
+ * GPU load is exactly what happens before them. */
13294
+ firstTokenTimeoutMs: number().int().positive().default(12e4),
13295
+ /** Output started then stopped. */
13296
+ idleTimeoutMs: number().int().positive().default(6e4),
13297
+ /** Profile-level default. The per-consumer table and a per-call override
13298
+ * both beat it — see `resolveRetryPolicy`. */
13299
+ retry: LlmRetryPolicySchema.default({
13300
+ enabled: false,
13301
+ maxAttempts: 1
13302
+ }),
13303
+ /** Whether this profile may use tools. The tool-call plumbing rides the
13304
+ * library; the REGISTRY of callable tools is ours and is empty in v1, so a
13305
+ * `true` here buys the wiring, not behaviour, until tools are registered. */
13306
+ toolsEnabled: boolean().default(false),
13100
13307
  extraHeaders: record(string(), string()).optional(),
13101
13308
  /** kind === 'managed-local' only (spec §4). */
13102
13309
  runtime: ManagedRuntimeConfigSchema.optional()
@@ -13146,6 +13353,36 @@ var ManagedModelCatalogEntrySchema = object({
13146
13353
  /** Vision models: companion projector file. */
13147
13354
  mmprojUrl: string().optional()
13148
13355
  });
13356
+ /**
13357
+ * The outcome of turning one operator-typed Hugging Face reference into a
13358
+ * download plan. A RESULT, never a throw: "this repo has 24 quantizations and
13359
+ * I will not pick for you" is a normal answer the UI has to render, not an
13360
+ * exception.
13361
+ *
13362
+ * `candidates` is the whole reason the refusal is usable — every string in it
13363
+ * is a tag that resolves when pasted back as `<org>/<repo>:<TAG>`.
13364
+ */
13365
+ var HfModelResolutionSchema = discriminatedUnion("ok", [object({
13366
+ ok: literal(true),
13367
+ /** Ready to hand to `installModel` unchanged. */
13368
+ model: ManagedModelRefSchema,
13369
+ label: string(),
13370
+ repo: string(),
13371
+ quantization: string(),
13372
+ purpose: _enum(["text", "vision"]),
13373
+ totalBytes: number(),
13374
+ /** mmproj + shards, for the preview: an operator approving 23 GB should
13375
+ * see that 0.9 GB of it is a projector they did not name. */
13376
+ extraFilenames: array(string())
13377
+ }), object({
13378
+ ok: literal(false),
13379
+ code: string(),
13380
+ message: string(),
13381
+ candidates: array(string()).optional(),
13382
+ /** Set when the refusal was only the ceiling: re-calling with
13383
+ * `maxBytes: requiredBytes` is the operator's explicit override. */
13384
+ requiredBytes: number().optional()
13385
+ })]);
13149
13386
  var LlmRuntimeNodeSchema = object({
13150
13387
  nodeId: string(),
13151
13388
  reachable: boolean(),
@@ -13158,7 +13395,10 @@ var ProfileRefInputSchema = object({
13158
13395
  addonId: string(),
13159
13396
  profileId: string()
13160
13397
  });
13161
- method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
13398
+ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({
13399
+ addonId: string().optional(),
13400
+ requestId: string()
13401
+ }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
13162
13402
  kind: "mutation",
13163
13403
  auth: "admin"
13164
13404
  }), method(ProfileRefInputSchema, _void(), {
@@ -13179,6 +13419,15 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13179
13419
  consumer: string().optional(),
13180
13420
  profileId: string().optional()
13181
13421
  }), array(LlmUsageRollupSchema)), method(object({}), array(ManagedModelCatalogEntrySchema)), method(object({}), array(LlmRuntimeNodeSchema)), method(object({ nodeId: string() }), array(LlmNodeModelSchema)), method(object({
13422
+ /** `https://huggingface.co/<org>/<repo>/resolve/main/<f>.gguf`,
13423
+ * `<org>/<repo>/<f>.gguf`, `<org>/<repo>` or `<org>/<repo>:<QUANT>`. */
13424
+ ref: string(),
13425
+ /** Explicit ceiling override, in bytes. Absent = the built-in ceiling. */
13426
+ maxBytes: number().positive().optional()
13427
+ }), HfModelResolutionSchema, {
13428
+ kind: "mutation",
13429
+ auth: "admin"
13430
+ }), method(object({
13182
13431
  nodeId: string(),
13183
13432
  model: ManagedModelRefSchema
13184
13433
  }), _void(), {
@@ -14788,6 +15037,8 @@ var NcSystemEventKindSchema = _enum([
14788
15037
  "stream-offline",
14789
15038
  "node-online",
14790
15039
  "node-offline",
15040
+ "node-inference-unavailable",
15041
+ "detection-blind",
14791
15042
  "addon-update-available",
14792
15043
  "server-update-available",
14793
15044
  "alarm-triggered",
@@ -14849,7 +15100,16 @@ var NcScheduleSchema = object({
14849
15100
  });
14850
15101
  /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
14851
15102
  var NcPlateMatcherSchema = object({
14852
- values: array(string().min(1)).min(1),
15103
+ /**
15104
+ * Plate texts (or gallery vehicle names) to match. EMPTY = **any plate the
15105
+ * pipeline could read** — the plate half of "no selection = no narrowing",
15106
+ * and the switch that says this rule is about vehicles that were IDENTIFIED
15107
+ * rather than merely seen. A subject carrying no plate still fails.
15108
+ *
15109
+ * The `.min(1)` this used to carry made that state unauthorable; nothing has
15110
+ * ever persisted an empty list, so widening it cannot change an existing rule.
15111
+ */
15112
+ values: array(string().min(1)),
14853
15113
  /** Max Levenshtein distance after normalization (uppercase alphanumeric). */
14854
15114
  maxDistance: number().int().min(0).max(3).default(1)
14855
15115
  });
@@ -14883,28 +15143,36 @@ var NcOccupancyConditionSchema = object({
14883
15143
  /**
14884
15144
  * Audio condition (IMMEDIATE trigger) — a rule on SOUND, not on a picture.
14885
15145
  *
14886
- * Operator-approved vocabulary (2026-08-12, option A — the same one the
14887
- * reference notifier uses, so an operator moving between them re-uses what
14888
- * they already know): a rule matches when, over a sampling window of
14889
- * `samplingSeconds`, at least `hitPercent`% of the audio samples in that
14890
- * window are HITS. A sample is a hit when it satisfies BOTH present filters:
14891
- *
14892
- * - `dbThreshold` its level is at or above this many dBFS (see
14893
- * {@link NC_AUDIO_DBFS_FLOOR}: negative-going, `0` = full scale);
14894
- * - `labels` the classifier put at least one of these labels on it.
14895
- *
14896
- * Both are OPTIONAL and independent, which is the point of the shape: a
14897
- * loudness rule ("something loud at 3am") needs no model to be right, and a
14898
- * label rule ("a dog barked") needs no threshold. **Fail-closed when NEITHER
14899
- * is given** a window in which every sample is trivially a hit would fire on
14900
- * silence, so the engine refuses such a condition rather than notifying on
14901
- * nothing (the schema cannot express "at least one of" without becoming a
14902
- * ZodEffects the cap path would have to special-case).
14903
- *
14904
- * `hitPercent` is over the samples the window actually HOLDS, and the window
14905
- * must be FULL before it can match a window that has been open for two
14906
- * seconds of its ten is 100% of nothing, and firing on it would make
14907
- * `samplingSeconds` decorative.
15146
+ * **TWO EXCLUSIVE MODES** (operator decision 2026-08-14, D157). Which one a
15147
+ * rule is in is not a stored field it is WHICH FILTER the rule carries, so
15148
+ * there is no second switch that can disagree with the first and every rule
15149
+ * authored before the decision migrates for free (`audioModeOf`):
15150
+ *
15151
+ * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
15152
+ * classifier labels with one of them. No window, no percentage:
15153
+ * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
15154
+ * `throttle` cooldown is the only brake. The per-label confidence floor is
15155
+ * the analyzer's (`classificationMinScore`, per device) — a label only
15156
+ * reaches this condition if the classifier was already confident enough.
15157
+ * - **LEVEL mode `dbThreshold` present, no labels.** The sampling window IS
15158
+ * the condition: at least `hitPercent`% of the samples over
15159
+ * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
15160
+ * {@link NC_AUDIO_DBFS_FLOOR}: negative-going, `0` = full scale). The window
15161
+ * must be FULL before it can match a window open for two of its ten
15162
+ * seconds is 100% of nothing.
15163
+ *
15164
+ * **Why label mode has no window.** It had one, and it never fired: the
15165
+ * analyzer emits ~1 audio frame per second but YAMNet only LABELS one to three
15166
+ * of them per episode, even through continuous crying. The measured maximum
15167
+ * `hitPercent` over the whole live history was 40 — under the shipped default
15168
+ * of 60, so a label rule could not fire at all, ever. A percentage of frames is
15169
+ * the wrong question to ask of a sparse classifier.
15170
+ *
15171
+ * **Fail-closed when NEITHER is given** — every sample would be a trivial hit
15172
+ * and the rule would fire on silence. The schema cannot express "exactly one
15173
+ * of" without becoming a ZodEffects the cap path would have to special-case, so
15174
+ * the exclusivity is enforced where every editor writes (`patchAudio`) and a
15175
+ * legacy rule carrying both resolves to LABEL (the mode that fires).
14908
15176
  *
14909
15177
  * Labels are the audio macro classes (`AUDIO_MACRO_LABELS` / the NC taxonomy's
14910
15178
  * `audio-*` ids). Both spellings are accepted — the matcher normalizes the
@@ -14912,13 +15180,13 @@ var NcOccupancyConditionSchema = object({
14912
15180
  * an operator who typed `dog` mean the same thing.
14913
15181
  */
14914
15182
  var NcAudioConditionSchema = object({
14915
- /** Audio macro labels; absent = any sound (level-only rule). */
15183
+ /** LABEL MODE: audio macro labels. Present fires on the first labelled frame. */
14916
15184
  labels: array(string().min(1)).min(1).optional(),
14917
- /** Level floor in dBFS (negative-going, `0` = full scale); absent = any level. */
15185
+ /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14918
15186
  dbThreshold: number().min(-96).max(0).optional(),
14919
- /** Percentage of the window's samples that must be hits (1–100). */
15187
+ /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14920
15188
  hitPercent: number().int().min(1).max(100).default(60),
14921
- /** Length of the sampling window in seconds. */
15189
+ /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14922
15190
  samplingSeconds: number().int().min(1).max(300).default(10)
14923
15191
  });
14924
15192
  /**
@@ -15056,13 +15324,81 @@ var NcRuleActionsSchema = object({
15056
15324
  */
15057
15325
  buttons: array(NcRuleNotificationButtonSchema).max(8).optional()
15058
15326
  });
15327
+ /**
15328
+ * "This rule applies only while `deviceId` is in one of `states`."
15329
+ *
15330
+ * The states are the DEVICE's own vocabulary — `AlarmState` for a panel,
15331
+ * `on`/`off` for a switch — not a normalised set, because normalising would
15332
+ * make the condition lie about devices whose states have no equivalent.
15333
+ *
15334
+ * An unreadable state does NOT match: see the engine's fail-closed gate. A
15335
+ * condition that fired on "I could not read it" would be worse than no gate.
15336
+ */
15337
+ var NcDeviceStateConditionSchema = object({
15338
+ deviceId: number().int(),
15339
+ /** Any of these matches. */
15340
+ states: array(string().min(1)).min(1)
15341
+ });
15342
+ /**
15343
+ * "This rule applies only while scene `sceneId` is `matched` / `diverged`."
15344
+ *
15345
+ * A GATE, not a trigger. `occupancy` and `audio` each DISCRIMINATE their rule —
15346
+ * carrying one makes the rule fire on that subject and nothing else. Scene is
15347
+ * the other shape entirely, the `deviceState` shape: it narrows a rule that
15348
+ * already has a trigger ("tell me about a person at the front door, but only
15349
+ * while the bin is still out"). That is why it composes with every delivery
15350
+ * instead of owning one, and why no new `NcDelivery` member and no new subject
15351
+ * kind exist for it — see D159.
15352
+ *
15353
+ * ── Identity ───────────────────────────────────────────────────────────────
15354
+ * `sceneId` is `SceneMonitor.id`, a `randomUUID()` minted by `createScene` —
15355
+ * globally unique, so it needs no device to disambiguate it. `deviceId` is
15356
+ * carried as a HINT for the editor and for the log line, never as part of the
15357
+ * lookup key: a rule whose hint drifted must still gate correctly.
15358
+ *
15359
+ * ── Which boolean ──────────────────────────────────────────────────────────
15360
+ * `latched` ABSENT means "whatever the scene itself says" — `SceneMonitor.emit`
15361
+ * already declares which boolean drives notification rules, and a second knob
15362
+ * that could disagree with it is exactly the D62 failure. Set it only to
15363
+ * override one rule against the scene's own default.
15364
+ *
15365
+ * - LIVE reading (`emit`/`latched` resolve to live): passes iff
15366
+ * `verdict === requiredState`. `unknown` — no reference for this light, view
15367
+ * shifted, no snapshot — passes NEITHER. A scene that cannot judge is not
15368
+ * evidence, in either direction.
15369
+ * - LATCHED reading: passes iff `latched === (requiredState === 'diverged')`.
15370
+ * The latch is a durable fact about the past ("it has diverged since I armed
15371
+ * it"), so a camera that has gone dark does not clear it — that is the whole
15372
+ * reason the operator asked for a latch.
15373
+ *
15374
+ * The gate reads an in-memory mirror (`NcSceneStateCache`) refreshed OFF the
15375
+ * event path, never the cap: D49. A mirror that has never loaded, or a scene it
15376
+ * does not carry, reads absent and the rule does NOT fire — fail closed, and
15377
+ * said out loud in the log rather than dropped in silence.
15378
+ */
15379
+ var NcSceneConditionSchema = object({
15380
+ /** `SceneMonitor.id` — the uuid the cap mints. The whole lookup key. */
15381
+ sceneId: string().min(1),
15382
+ /** The camera the scene lives on. A hint for the editor and the log line. */
15383
+ deviceId: number().int().optional(),
15384
+ /** The state the scene must be in for the rule to fire. */
15385
+ requiredState: _enum(["matched", "diverged"]),
15386
+ /**
15387
+ * Read the LATCH (`true`) or the LIVE verdict (`false`). Absent = follow the
15388
+ * scene's own `emit` field, which is the only place that decision belongs.
15389
+ */
15390
+ latched: boolean().optional()
15391
+ });
15059
15392
  var NcConditionsSchema = object({
15060
15393
  /** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
15061
- deviceState: object({
15062
- deviceId: number().int(),
15063
- /** Any of these matches. */
15064
- states: array(string().min(1)).min(1)
15065
- }).optional(),
15394
+ deviceState: NcDeviceStateConditionSchema.optional(),
15395
+ /**
15396
+ * Gate on a SCENE's state — "only while the bin is still out". Composes with
15397
+ * every trigger (detection, occupancy, audio, sensor, package, track-end);
15398
+ * unlike `occupancy`/`audio` it discriminates nothing. See
15399
+ * {@link NcSceneCondition} and D159.
15400
+ */
15401
+ scene: NcSceneConditionSchema.optional(),
15066
15402
  /** Device scope — absent = all devices. */
15067
15403
  devices: array(number()).optional(),
15068
15404
  /** Detector class names (any overlap with the record's class set). */
@@ -15088,18 +15424,47 @@ var NcConditionsSchema = object({
15088
15424
  */
15089
15425
  labelEquals: array(string().min(1)).optional(),
15090
15426
  /**
15091
- * Identity matcher. P1 boundary: matched against the record's collapsed
15092
- * `label` (the identity display name propagated by the face pipeline) —
15093
- * identity-ID matching rides in P2 when identity ids reach the record.
15427
+ * KNOWN FACES the rule's identity scope, and the switch that says the rule
15428
+ * is about recognised people at all.
15429
+ *
15430
+ * Three states, and the empty one is the point:
15431
+ *
15432
+ * | value | meaning |
15433
+ * | --- | --- |
15434
+ * | absent | the rule does not care who it is; an unrecognised person matches |
15435
+ * | `[]` | **only known faces** — any identity in the gallery, nobody in particular |
15436
+ * | a list | only these identities |
15437
+ *
15438
+ * `[]` is the repo-wide "no selection = no narrowing" reading (an absent
15439
+ * `devices` list is every device), applied one level down: the operator has
15440
+ * turned the face scope ON and narrowed it to nothing, which is every known
15441
+ * face. No second field states the same thing — a switch that can disagree
15442
+ * with the list under it is worse than no switch (D62).
15443
+ *
15444
+ * MEMBERS ARE FACE-GALLERY `Identity.id`s (uuid), not display names. A name is
15445
+ * renameable, and a rule authored on "Gianluca" went silently dark the moment
15446
+ * the operator fixed the spelling. The id reaches the record on
15447
+ * `LabelAttribution.identityId`; the name is what the editor shows and what
15448
+ * `{{label}}` renders.
15449
+ *
15450
+ * Rules written before this carry NAMES, and are resolved to ids lazily at
15451
+ * load (`NcRuleStore.load`) against the live gallery — a name nothing answers
15452
+ * for is left as it stands and reported, never dropped. The engine also
15453
+ * accepts a display-name hit as a compatibility leg, so a rule whose
15454
+ * migration could not resolve keeps matching exactly what it matched before.
15094
15455
  */
15095
15456
  identities: array(string().min(1)).optional(),
15096
- /** Fuzzy plate matcher against the record's `label` (plate text). */
15457
+ /**
15458
+ * KNOWN PLATES / VEHICLES — the plate mirror of {@link identities}, including
15459
+ * the empty-list reading: `values: []` is "any plate the OCR could read",
15460
+ * a non-empty list is those plates (fuzzily). See {@link NcPlateMatcherSchema}.
15461
+ */
15097
15462
  plates: NcPlateMatcherSchema.optional(),
15098
15463
  /**
15099
- * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics.
15100
- * Same P1 boundary: matched against the record's collapsed `label` (the
15101
- * identity display name). A record with NO label passes (nothing to
15102
- * exclude), unlike the include variant which fails on an absent label.
15464
+ * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics, and
15465
+ * the same id members and the same lazy name→id migration. A record with NO
15466
+ * identity passes (nothing to exclude), unlike the include variant which
15467
+ * fails on an unrecognised subject. An EMPTY list excludes nobody.
15103
15468
  */
15104
15469
  identitiesExclude: array(string().min(1)).optional(),
15105
15470
  /**
@@ -15491,7 +15856,80 @@ var NcRuleInputSchema = object({
15491
15856
  * a rule that predates the gate must keep delivering byte-for-byte as it
15492
15857
  * did, and absent is the only way to say that without a migration.
15493
15858
  */
15494
- confirm: NcConfirmSchema.optional()
15859
+ confirm: NcConfirmSchema.optional(),
15860
+ /**
15861
+ * WAIT for face/plate recognition before saying anything.
15862
+ *
15863
+ * A notification's TEXT is frozen at enqueue and its media is re-resolved at
15864
+ * send; the identity is neither. A face is confirmed after `confirmFrames`
15865
+ * agreeing observations — p50 **11.4 s** after the track was first seen,
15866
+ * measured on this hub — and an `immediate` rule enqueues on the first object
15867
+ * event, seconds before that. So "Gianluca è arrivato" is unsayable on the
15868
+ * immediate path, and no amount of media re-resolution fixes a sentence.
15869
+ *
15870
+ * Only two honest answers exist, and this flag picks between them. It has
15871
+ * effect ONLY on a rule that declares a recognition scope
15872
+ * ({@link NcConditions.identities} or {@link NcConditions.plates}) — on any
15873
+ * other rule there is nothing to wait for and the flag is inert.
15874
+ *
15875
+ * | value | what happens |
15876
+ * | --- | --- |
15877
+ * | `true` | the rule stops firing on the object event and fires at TRACK CLOSE instead, once, with the name — later, and complete |
15878
+ * | absent / `false` | it fires at once WITHOUT the name, and if recognition lands before the track closes a SECOND, "…is Gianluca" notification follows (one per track, per rule, per target) |
15879
+ *
15880
+ * `.optional()` and deliberately NOT `.default()`: a Zod default does not run
15881
+ * on the addon cap path, and absent has to keep meaning exactly what every
15882
+ * rule authored before this field meant.
15883
+ *
15884
+ * The cost of `true` is stated here because the editor states it too: a rule
15885
+ * that waits also inherits track-close SEMANTICS — its `zones` condition
15886
+ * tests every zone the track visited and a `crossing` condition can no longer
15887
+ * be satisfied, because a closed track carries no crossing.
15888
+ */
15889
+ waitForEnhancement: boolean().optional(),
15890
+ /**
15891
+ * GROUP a burst of subjects into ONE notification that grows.
15892
+ *
15893
+ * Seconds of quiet after the last matching subject before the burst is
15894
+ * considered over. While it is open, the first subject enqueues immediately —
15895
+ * **exactly as today, with no added latency** — and every real growth (a new
15896
+ * subject, or a name confirmed on one already in it) REPLACES that
15897
+ * notification with an updated one naming everybody. The push carries the
15898
+ * group's own coalescing tag, so the phone replaces rather than stacks.
15899
+ *
15900
+ * `0` / absent = off, and off is today's behaviour byte for byte.
15901
+ *
15902
+ * ### Why an idle cutoff and not a window
15903
+ *
15904
+ * The measured seven-person arrival on device 590 spans 110 s with every
15905
+ * internal gap under 30 s. A 12 s fixed window cuts it into three groups; an
15906
+ * idle cutoff holds it as one and ends it when the arrival actually ends.
15907
+ * 30 is Frigate's shipped value for the same decision.
15908
+ *
15909
+ * ### What it replaces
15910
+ *
15911
+ * The blind cooldown, which collapses a burst by DISCARDING it. Measured on
15912
+ * device 615 / *Persona su Uscio* over six days: 116 qualifying tracks → 74
15913
+ * notifications, **44 (37.9%) suppressed outright**, 23 of them overlapping a
15914
+ * track that did fire and 7 carrying a confirmed identity nobody heard about.
15915
+ * A group collapses the same volume by MERGING, so the cooldown becomes a
15916
+ * budget over GROUPS — which is what it always meant — and a growth is never
15917
+ * throttled by the window its own first member spent.
15918
+ *
15919
+ * ### Interaction with {@link waitForEnhancement}
15920
+ *
15921
+ * They compose, and the order matters. `waitForEnhancement` defers the rule to
15922
+ * TRACK CLOSE, so with both set the group is opened by the first member to
15923
+ * CLOSE — already carrying its name — and grows as later members close. That
15924
+ * is later, and complete. With grouping alone the group opens on the first
15925
+ * object event and picks up names as they are confirmed, through the growth
15926
+ * path. Neither combination fires twice for one subject.
15927
+ *
15928
+ * `.optional()` and deliberately NOT `.default()`: a Zod default does not run
15929
+ * on the addon cap path, so absent must keep meaning what it meant before this
15930
+ * field existed.
15931
+ */
15932
+ groupIdleSec: number().int().min(0).max(600).optional()
15495
15933
  });
15496
15934
  /**
15497
15935
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -15598,6 +16036,7 @@ var NcConditionDescriptorSchema = object({
15598
16036
  "occupancy",
15599
16037
  "audio",
15600
16038
  "deviceState",
16039
+ "scene",
15601
16040
  "systemEvent"
15602
16041
  ]),
15603
16042
  operator: _enum([
@@ -16003,7 +16442,87 @@ var MethodAccessSchema = _enum([
16003
16442
  var AllowedProviderSchema = union([literal("*"), array(string())]);
16004
16443
  var AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
16005
16444
  var CapScopeSchema = _enum(["device", "system"]);
16006
- var TokenScopeSchema = discriminatedUnion("type", [
16445
+ /**
16446
+ * DeviceSelector (scope model v3 — 2026-08-12).
16447
+ *
16448
+ * A `device` grant no longer carries a frozen list of deviceIds. It carries
16449
+ * a SELECTOR the matcher resolves against the live fleet, so the grant can be
16450
+ * DYNAMIC: a `types:['camera']` selector automatically covers a camera added
16451
+ * AFTER the grant was minted — no re-grant, no re-login.
16452
+ *
16453
+ * - `all` — every device in the deployment. The broad viewer/operator
16454
+ * lever without a `category` grant (a `category` grant also covers device
16455
+ * caps that carry no deviceId; `all` is specifically the device set).
16456
+ * - `ids` — an explicit deviceId list. This is what a v2 `device:[…]`
16457
+ * grant migrates to (see {@link TokenScopeSchema}); STATIC — a new camera
16458
+ * is NOT covered until the grant is edited.
16459
+ * - `types` — every device of a `DeviceType` (e.g. every `camera`).
16460
+ * DYNAMIC. A device that changes type, or a new device of the type,
16461
+ * re-resolves on the next request.
16462
+ * - `locations` — every device whose operator-assigned `location` label is
16463
+ * in the set (e.g. "Garden", "Front door"). DYNAMIC. A device with a
16464
+ * null/unset location matches NO `locations` selector.
16465
+ */
16466
+ var DeviceSelectorSchema = discriminatedUnion("kind", [
16467
+ object({ kind: literal("all") }),
16468
+ object({
16469
+ kind: literal("ids"),
16470
+ ids: array(number().int()).min(1)
16471
+ }),
16472
+ object({
16473
+ kind: literal("types"),
16474
+ types: array(_enum(DeviceType)).min(1)
16475
+ }),
16476
+ object({
16477
+ kind: literal("locations"),
16478
+ locations: array(string().min(1)).min(1)
16479
+ })
16480
+ ]);
16481
+ var DeviceTokenScopeSchema = object({
16482
+ type: literal("device"),
16483
+ /** The device SET this grant covers — resolved against the live fleet. */
16484
+ selector: DeviceSelectorSchema,
16485
+ access: array(MethodAccessSchema).min(1),
16486
+ /**
16487
+ * Whether a grant on a PARENT device transparently covers its accessory
16488
+ * CHILDREN (siren / floodlight / PIR) via the persisted-parentage walk.
16489
+ * Direction is parent → children ONLY.
16490
+ *
16491
+ * Absent → the matcher DERIVES it from the access flavour: `view`
16492
+ * inherits (a camera viewer sees the camera's accessories), `create` /
16493
+ * `delete` do NOT (actuating/removing a child is an explicit act the
16494
+ * operator must grant on the child, not inherit from the parent). Set it
16495
+ * explicitly to override that default per grant.
16496
+ */
16497
+ includeLinked: boolean().optional()
16498
+ });
16499
+ /**
16500
+ * v2 → v3 lazy migration. A pre-v3 `device` grant carried
16501
+ * `targets: string[]` (stringified deviceIds); it rewrites to the equivalent
16502
+ * `selector: {kind:'ids', ids}`. Applied as a `preprocess` so it runs on
16503
+ * EVERY parse path — stored records AND the JWT-carried scope arrays
16504
+ * normalised at the request boundary ({@link normalizeTokenScopes} in
16505
+ * `device-selector.ts`). Chosen over a one-time DB migration because a
16506
+ * migration cannot reach a JWT already in a client's hands; parse-time
16507
+ * migration covers both without a flag day. No cast — the raw object is read
16508
+ * through `Reflect.get` (its static type is `unknown`).
16509
+ */
16510
+ function migrateLegacyTokenScope(raw) {
16511
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
16512
+ if (Reflect.get(raw, "type") !== "device") return raw;
16513
+ if (Reflect.get(raw, "selector") !== void 0) return raw;
16514
+ const targets = Reflect.get(raw, "targets");
16515
+ if (!Array.isArray(targets)) return raw;
16516
+ return {
16517
+ type: "device",
16518
+ selector: {
16519
+ kind: "ids",
16520
+ ids: targets.map((t) => typeof t === "string" ? Number(t) : t).filter((n) => typeof n === "number" && Number.isInteger(n))
16521
+ },
16522
+ access: Reflect.get(raw, "access")
16523
+ };
16524
+ }
16525
+ var TokenScopeSchema = preprocess(migrateLegacyTokenScope, discriminatedUnion("type", [
16007
16526
  object({
16008
16527
  type: literal("category"),
16009
16528
  target: CapScopeSchema,
@@ -16019,18 +16538,8 @@ var TokenScopeSchema = discriminatedUnion("type", [
16019
16538
  target: string(),
16020
16539
  access: array(MethodAccessSchema).min(1)
16021
16540
  }),
16022
- object({
16023
- type: literal("device"),
16024
- /**
16025
- * One or more deviceIds (serialised as strings for wire-format
16026
- * consistency with the rest of the union). Matcher accepts if
16027
- * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
16028
- * of one scope-per-device when granting access to a set of cameras.
16029
- */
16030
- targets: array(string()).min(1),
16031
- access: array(MethodAccessSchema).min(1)
16032
- })
16033
- ]);
16541
+ DeviceTokenScopeSchema
16542
+ ]));
16034
16543
  object({
16035
16544
  id: string(),
16036
16545
  username: string(),
@@ -16347,7 +16856,7 @@ var TrackEnvelopeSchema = object({
16347
16856
  * `snapshots[]` references — megabytes across a page of tracks. `slim`
16348
16857
  * keeps every scalar the list surfaces actually render (ids, class(es),
16349
16858
  * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
16350
- * zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
16859
+ * zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
16351
16860
  * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
16352
16861
  * `getTrack`. Mirrors the event-store `projection` convention
16353
16862
  * (`getObjectEvents` et al.).
@@ -16483,7 +16992,21 @@ union([literal(1), literal(2)]);
16483
16992
  var LabelAttributionSchema = object({
16484
16993
  stepId: string(),
16485
16994
  modelId: string().optional(),
16486
- decidedAt: number()
16995
+ decidedAt: number(),
16996
+ /**
16997
+ * The GALLERY id behind a recognised tier-2 label — a face-gallery
16998
+ * `Identity.id` or a plate-gallery `Vehicle.id` (both `randomUUID`).
16999
+ *
17000
+ * The text alone is a DISPLAY NAME, and a display name is renameable: a
17001
+ * notification rule authored on "Gianluca" stopped matching the moment the
17002
+ * operator fixed the spelling in the gallery, and nothing said so. The id is
17003
+ * the thing that does not move, so it is what a rule matches on
17004
+ * (`NcConditions.identities`) and the text is what a human is shown.
17005
+ *
17006
+ * Absent when the label names no gallery row — a plate the OCR read but no
17007
+ * vehicle claims, a sub-class, a species, any tier-1 value.
17008
+ */
17009
+ identityId: string().optional()
16487
17010
  });
16488
17011
  /**
16489
17012
  * The TIERED label model (roadmap 4g), spread into `TrackSchema` and
@@ -16620,6 +17143,28 @@ var TrackSchema = object({
16620
17143
  * `=== true` and render nothing otherwise, never infer "no face".
16621
17144
  */
16622
17145
  hasFace: boolean().optional(),
17146
+ /**
17147
+ * This subject CONTAINS a folded rider — a person the rider-pairing step
17148
+ * ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
17149
+ * so the passage is tracked once and as a VEHICLE.
17150
+ *
17151
+ * It exists because the fold's record was dishonest. D34 and the code both
17152
+ * said "the person is not lost — it is reported so both entities stay on the
17153
+ * record"; in fact the pair went into a per-processor RAM field behind an
17154
+ * accessor nobody called, and every durable surface said `vehicle`, full
17155
+ * stop. This is the composition note that makes the row true.
17156
+ *
17157
+ * A COMPOSITION, never a class and never a label. "This vehicle contains a
17158
+ * person" is not an answer to "what is this" — both label tiers would refuse
17159
+ * a macro token anyway (D89), and correctly. Nothing here changes what the
17160
+ * subject IS: a cyclist stays one vehicle track, occupancy still counts one,
17161
+ * and a `person` rule still does not fire for someone cycling past.
17162
+ *
17163
+ * **Absent ≠ false**, exactly like {@link hasFace}: every row written before
17164
+ * the column, and every hub that predates the field, omits it. Test
17165
+ * `=== true` and render nothing otherwise — never infer "no rider".
17166
+ */
17167
+ hasRider: boolean().optional(),
16623
17168
  ...TrackFlagFields,
16624
17169
  ...TrackRetrainFields
16625
17170
  });
@@ -16969,7 +17514,10 @@ var RecentTracksQueryInput = object({
16969
17514
  * Encodes the (lastSeen, trackId) sort position — treat as opaque. */
16970
17515
  cursor: string().optional(),
16971
17516
  /** See {@link TrackProjectionSchema}. Default `full`. */
16972
- projection: TrackProjectionSchema.optional()
17517
+ projection: TrackProjectionSchema.optional(),
17518
+ /** Include stationary-promoted rows (parked objects). Default false: the
17519
+ * feed lists passages; parking records live on the stationary registry. */
17520
+ includeStationary: boolean().optional()
16973
17521
  });
16974
17522
  var RecentTracksPageSchema = object({
16975
17523
  /** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
@@ -17187,7 +17735,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17187
17735
  zone: TrackZoneFilterSchema.optional(),
17188
17736
  /** See {@link TrackProjectionSchema}. Default `full` (backward
17189
17737
  * compatible — omitting the field keeps today's exact behaviour). */
17190
- projection: TrackProjectionSchema.optional()
17738
+ projection: TrackProjectionSchema.optional(),
17739
+ /** Include stationary-promoted rows (parked objects handed to the
17740
+ * stationary registry). Default false: the timeline lists passages,
17741
+ * not parking records (operator decision, 2026-08-15). */
17742
+ includeStationary: boolean().optional()
17191
17743
  }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17192
17744
  kind: "mutation",
17193
17745
  auth: "admin"
@@ -17351,11 +17903,16 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17351
17903
  auth: "admin"
17352
17904
  }), method(object({
17353
17905
  eventId: string(),
17354
- kind: MediaFileKindEnum.optional()
17906
+ kind: MediaFileKindEnum.optional(),
17907
+ deviceId: number()
17908
+ }), array(MediaFileSchema).readonly()), method(object({
17909
+ trackId: string(),
17910
+ kinds: array(MediaFileKindEnum).optional(),
17911
+ deviceId: number()
17355
17912
  }), array(MediaFileSchema).readonly()), method(object({
17356
17913
  trackId: string(),
17357
- kinds: array(MediaFileKindEnum).optional()
17358
- }), array(MediaFileSchema).readonly()), method(object({ trackId: string() }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
17914
+ deviceId: number()
17915
+ }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
17359
17916
  kind: "mutation",
17360
17917
  auth: "admin"
17361
17918
  }), method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
@@ -18015,6 +18572,17 @@ var maxSessionHoldMsField = {
18015
18572
  default: 12e4,
18016
18573
  step: 5e3
18017
18574
  };
18575
+ /**
18576
+ * Quiet period that closes an `audioMode: 'on-motion'` audio window. Floor of
18577
+ * 5s so a rearm can never degenerate into per-event stream churn; default 90s
18578
+ * comfortably outlives the gap between two PIR wakes on a battery camera.
18579
+ */
18580
+ var audioMotionWindowMsField = {
18581
+ min: 5e3,
18582
+ max: 6e5,
18583
+ default: 9e4,
18584
+ step: 5e3
18585
+ };
18018
18586
  var motionFpsField = {
18019
18587
  min: 1,
18020
18588
  max: 30,
@@ -18046,7 +18614,7 @@ var detectionFpsField = {
18046
18614
  var occupancyRecheckSecField = {
18047
18615
  min: 0,
18048
18616
  max: 300,
18049
- default: 30,
18617
+ default: 300,
18050
18618
  step: 5
18051
18619
  };
18052
18620
  var occupancyRecheckFramesField = {
@@ -18191,6 +18759,27 @@ var RunnerCameraConfigSchema = object({
18191
18759
  * resolved `CameraDetectionConfig`.
18192
18760
  */
18193
18761
  maxSessionHoldMs: number().min(maxSessionHoldMsField.min).max(maxSessionHoldMsField.max).optional(),
18762
+ /**
18763
+ * Orchestrator-side quiet period (ms) that closes an `audioMode:
18764
+ * 'on-motion'` audio window, measured from the LAST motion event.
18765
+ *
18766
+ * This exists because the falling edge cannot be relied on. Camera-native
18767
+ * providers emit motion as a RISING EDGE ONLY (Reolink's Baichuan push and
18768
+ * its email-push SMTP path both emit `detected: true` and never the
18769
+ * counterpart); only the frame-diff analyzer emits falls. So on an
18770
+ * onboard-only camera a window that closed only on `detected: false` never
18771
+ * closed at all, and `on-motion` silently behaved as `always-on` — on a
18772
+ * battery camera, the one failure mode the mode exists to prevent.
18773
+ *
18774
+ * Every motion event rearms this timer WITHOUT restarting the stream, so a
18775
+ * burst of re-fires costs nothing. A falling edge, when one does arrive,
18776
+ * still closes earlier via `motionCooldownMs` — whichever comes first wins.
18777
+ *
18778
+ * Not consumed by the runner: carried here so it shares the per-camera
18779
+ * device-settings surface with `motionCooldownMs`, exactly like
18780
+ * `maxSessionHoldMs`.
18781
+ */
18782
+ audioMotionWindowMs: number().min(audioMotionWindowMsField.min).max(audioMotionWindowMsField.max).optional(),
18194
18783
  motionFps: number().min(motionFpsField.min).max(motionFpsField.max).default(motionFpsField.default),
18195
18784
  detectionFps: number().min(detectionFpsField.min).max(detectionFpsField.max).default(detectionFpsField.default),
18196
18785
  motionStreamId: string(),
@@ -18286,7 +18875,7 @@ var RunnerCameraConfigSchema = object({
18286
18875
  */
18287
18876
  inferenceDevices: array(RunnerInferenceDeviceSchema).readonly().optional()
18288
18877
  });
18289
- motionFpsField.min, motionFpsField.max, motionFpsField.step, motionFpsField.default, detectionFpsField.min, detectionFpsField.max, detectionFpsField.step, detectionFpsField.default, motionCooldownMsField.min, motionCooldownMsField.max, motionCooldownMsField.step, motionCooldownMsField.default, maxSessionHoldMsField.min, maxSessionHoldMsField.max, maxSessionHoldMsField.step, maxSessionHoldMsField.default, occupancyRecheckSecField.min, occupancyRecheckSecField.max, occupancyRecheckSecField.step, occupancyRecheckSecField.default, occupancyRecheckFramesField.min, occupancyRecheckFramesField.max, occupancyRecheckFramesField.step, occupancyRecheckFramesField.default;
18878
+ motionFpsField.min, motionFpsField.max, motionFpsField.step, motionFpsField.default, detectionFpsField.min, detectionFpsField.max, detectionFpsField.step, detectionFpsField.default, motionCooldownMsField.min, motionCooldownMsField.max, motionCooldownMsField.step, motionCooldownMsField.default, maxSessionHoldMsField.min, maxSessionHoldMsField.max, maxSessionHoldMsField.step, maxSessionHoldMsField.default, audioMotionWindowMsField.min, audioMotionWindowMsField.max, audioMotionWindowMsField.step, audioMotionWindowMsField.default, occupancyRecheckSecField.min, occupancyRecheckSecField.max, occupancyRecheckSecField.step, occupancyRecheckSecField.default, occupancyRecheckFramesField.min, occupancyRecheckFramesField.max, occupancyRecheckFramesField.step, occupancyRecheckFramesField.default;
18290
18879
  /**
18291
18880
  * Runtime load summary returned by `getLocalLoad`. Used by the orchestrator's
18292
18881
  * load-balancing levels (L2 capacity-based, L3 hardware-aware) to decide
@@ -19302,7 +19891,16 @@ targets: array(object({
19302
19891
  /** A sleeping battery camera: the frame is deliberately stale and will
19303
19892
  * NOT refresh in the background. A surface should say so rather than
19304
19893
  * present it as current. */
19305
- sleeping: boolean()
19894
+ sleeping: boolean(),
19895
+ /** Current device state rendered over the cached frame. State images
19896
+ * remain authoritative even when their photographic background is
19897
+ * old; null means the link must carry a current camera frame. */
19898
+ stateReason: _enum([
19899
+ "disabled",
19900
+ "sleeping",
19901
+ "unreachable",
19902
+ "waking"
19903
+ ]).nullable()
19306
19904
  })));
19307
19905
  /**
19308
19906
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
@@ -20659,7 +21257,11 @@ object({
20659
21257
  precision: number().int().min(0).max(10).optional()
20660
21258
  });
20661
21259
  DeviceType.Sensor;
20662
- object({
21260
+ /**
21261
+ * Ambient illuminance reading in lux. Drives Home Assistant `sensor`
21262
+ * entries with `device_class: illuminance`.
21263
+ */
21264
+ var AmbientLightSensorStatusSchema = object({
20663
21265
  /** Current illuminance in lux (lx). */
20664
21266
  lux: number().min(0),
20665
21267
  /** Ms epoch when the slice was last updated. */
@@ -20824,6 +21426,25 @@ var BatteryStatusSchema = object({
20824
21426
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
20825
21427
  lastUpdated: number(),
20826
21428
  /**
21429
+ * Ms epoch of the last time the device PROVED it was reachable — a
21430
+ * completed firmware round-trip, an observed wake, or an inbound push
21431
+ * (firmware event, email). `0`/absent = never since this slice was born.
21432
+ *
21433
+ * This is the ONLY input that separates "asleep" from "gone", and it is
21434
+ * fed exclusively by PASSIVE signals: nothing may write it by reaching
21435
+ * for the radio, because a poll that confirms reachability is the same
21436
+ * poll that drains the battery. See {@link deriveBatteryPresence} — the
21437
+ * single derivation every consumer must use; no surface computes its own.
21438
+ *
21439
+ * It is deliberately NOT a clock in the
21440
+ * `scripts/check-runtime-state-durability.ts` sense: it is the
21441
+ * observation itself, and it is the only thing a 30-hour silence is
21442
+ * visible in. Writers quantise it (see `CONTACT_WRITE_QUANTUM_MS` in the
21443
+ * Reolink provider) so a value that means "recently" cannot cost a
21444
+ * SQLite commit per round-trip.
21445
+ */
21446
+ lastContactAt: number().optional(),
21447
+ /**
20827
21448
  * True when the source is a BINARY low-battery indicator (HA
20828
21449
  * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
20829
21450
  * charge level — `percentage` is then a coarse stand-in (100 = normal,
@@ -20932,7 +21553,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), CameraCredentialsSchem
20932
21553
  kind: "query",
20933
21554
  auth: "admin"
20934
21555
  });
20935
- object({
21556
+ /**
21557
+ * Carbon-monoxide alarm sensor. Drives Home Assistant `binary_sensor`
21558
+ * entries with `device_class: carbon_monoxide`. Push-driven.
21559
+ */
21560
+ var CarbonMonoxideStatusSchema = object({
20936
21561
  detected: boolean(),
20937
21562
  /** Ms epoch of the last transition. 0 if never observed. */
20938
21563
  lastChangedAt: number()
@@ -21220,7 +21845,19 @@ Object.values(DeviceType), method(object({
21220
21845
  kind: "mutation",
21221
21846
  auth: "admin"
21222
21847
  }), ConsumablesStatusSchema.extend({ lastFetchedAt: number() });
21223
- object({
21848
+ /**
21849
+ * Door / window / opening / garage / valve contact sensor. Boolean
21850
+ * "is the entry currently open" with the timestamp of the last
21851
+ * transition. Drives Home Assistant `binary_sensor` entries whose
21852
+ * `device_class` is `door`, `window`, `opening`, `garage`, or
21853
+ * `garage_door` — and any future native integration that needs
21854
+ * the same semantics.
21855
+ *
21856
+ * Push-driven: providers update the slice on transition events from
21857
+ * the upstream source (HA WebSocket `state_changed`, ZWave
21858
+ * `notification` …). Consumers read the slice; no polling.
21859
+ */
21860
+ var ContactStatusSchema = object({
21224
21861
  /** True when the entry is open; false when closed. */
21225
21862
  entryOpen: boolean(),
21226
21863
  /** Ms epoch of the last open↔closed transition. 0 if never observed. */
@@ -21819,7 +22456,15 @@ object({
21819
22456
  deviceId: number(),
21820
22457
  status: FeatureProbeStatusSchema
21821
22458
  });
21822
- object({
22459
+ /**
22460
+ * Water leak / moisture sensor. Boolean "is liquid currently
22461
+ * detected" with the timestamp of the last transition. Drives Home
22462
+ * Assistant `binary_sensor` entries with `device_class: moisture`,
22463
+ * and any future native flood sensor.
22464
+ *
22465
+ * Push-driven from the upstream source.
22466
+ */
22467
+ var FloodStatusSchema = object({
21823
22468
  /** True when leak is currently detected. */
21824
22469
  flooded: boolean(),
21825
22470
  /** Ms epoch of the last flooded↔dry transition. 0 if never observed. */
@@ -21873,7 +22518,15 @@ DeviceType.Humidifier, method(object({
21873
22518
  kind: "mutation",
21874
22519
  auth: "admin"
21875
22520
  });
21876
- object({
22521
+ /**
22522
+ * Single-metric humidity reading. Drives Home Assistant `sensor`
22523
+ * entries with `device_class: humidity`.
22524
+ *
22525
+ * Unit normalisation: percent. The canonical display unit (`%`) is a
22526
+ * descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
22527
+ * `sourceInfo`.
22528
+ */
22529
+ var HumiditySensorStatusSchema = object({
21877
22530
  /** Current relative humidity, 0..100. */
21878
22531
  percent: number().min(0).max(100),
21879
22532
  /** Ms epoch when the slice was last updated. */
@@ -22495,7 +23148,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22495
23148
  * tunnel always emits `https://` regardless. */
22496
23149
  scheme: _enum(["http", "https"]).optional()
22497
23150
  }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
22498
- object({
23151
+ var LockControlStatusSchema = object({
22499
23152
  /** Lifecycle state of the lock. `jammed` means the motor reported
22500
23153
  * failure to reach the target — operator intervention required. */
22501
23154
  state: _enum([
@@ -22833,7 +23486,19 @@ authKey: string().optional() }), object({
22833
23486
  /** Human-readable error when `ok: false`. */
22834
23487
  error: string().optional()
22835
23488
  }), { kind: "mutation" });
22836
- object({
23489
+ /**
23490
+ * Hardware / firmware motion sensor cap — binary detected state plus
23491
+ * a timestamp of the last observation. Distinct from
23492
+ * `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
23493
+ * `motion` is the lightweight readout from on-camera motion (Reolink
23494
+ * `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
23495
+ *
23496
+ * Native-motion providers also fan out to `detection.camera-native`
23497
+ * with `source: 'onboard'` so cross-cutting system services
23498
+ * (alert-center, advanced-notifier) can subscribe once and receive
23499
+ * motion from every camera.
23500
+ */
23501
+ var MotionStatusSchema = object({
22837
23502
  detected: boolean(),
22838
23503
  /** Ms epoch of the last detected-true observation. Null if never detected. */
22839
23504
  lastDetectedAt: number().nullable(),
@@ -23945,7 +24610,7 @@ var GpsLocationSchema = object({
23945
24610
  /** Reported accuracy in meters (lower = better). */
23946
24611
  accuracyMeters: number().nonnegative()
23947
24612
  });
23948
- object({
24613
+ var PresenceStatusSchema = object({
23949
24614
  /** `home` / `not_home` / any user-defined zone name. */
23950
24615
  state: string(),
23951
24616
  /** Optional textual location label (zone name, city, address). Null
@@ -24361,7 +25026,7 @@ method(object({
24361
25026
  toMs: number()
24362
25027
  }), RecordingAvailabilitySchema, {
24363
25028
  kind: "query",
24364
- auth: "admin"
25029
+ auth: "protected"
24365
25030
  }), method(object({
24366
25031
  deviceId: number(),
24367
25032
  fromMs: number(),
@@ -24369,14 +25034,14 @@ method(object({
24369
25034
  tzOffsetMinutes: number()
24370
25035
  }), RecordingDaysSchema, {
24371
25036
  kind: "query",
24372
- auth: "admin"
25037
+ auth: "protected"
24373
25038
  }), method(object({
24374
25039
  deviceId: number(),
24375
25040
  fromMs: number(),
24376
25041
  toMs: number()
24377
25042
  }), RecordingManifestSchema, {
24378
25043
  kind: "query",
24379
- auth: "admin"
25044
+ auth: "protected"
24380
25045
  }), method(object({}), RecordingStorageUsageSchema, {
24381
25046
  kind: "query",
24382
25047
  auth: "admin"
@@ -24666,14 +25331,77 @@ method(object({
24666
25331
  * thing except the comparator: `similarity` (CLIP cosine at the same ROI coords
24667
25332
  * vs condition-tagged references) and `llm` (vision-LLM judgment over the crop).
24668
25333
  *
24669
- * D14 device-config archetype (`deviceConfig.ui.kind:'widget'`) the framework
24670
- * derives the device-detail contribution; the provider carries NO hand-written
24671
- * settings-contribution methods. `status.kind:'push'` the engine pushes on
24672
- * every hysteresis flip / availability change; consumers never poll.
25334
+ * **No `deviceConfig`, deliberately.** This shipped as the D14 widget archetype,
25335
+ * which put a "Scenes" tab on one camera's detail page. That is the wrong shape
25336
+ * for the thing: a scene is a standing question about the property ("is the bin
25337
+ * still out"), and the operator's question is "which of my scenes have tripped",
25338
+ * across every camera at once — not "what does camera 617 think". Buried one
25339
+ * camera deep it also could not be found. The surface is now a top-level admin
25340
+ * page (`/scenes`, `pages/Scenes.tsx`) that lists every scene on every camera and
25341
+ * picks the camera inside the create flow, the same shape Events and Faces have.
25342
+ *
25343
+ * The consequence to keep in mind: `host/scene-monitor-editor` is gone from
25344
+ * `HOST_WIDGETS` too. `scripts/check-host-widget-resolves.ts` asserts BOTH
25345
+ * directions, so a registration nobody declares fails exactly as loudly as a
25346
+ * declaration nobody registers. The editor is imported directly by the page.
25347
+ *
25348
+ * `status.kind:'push'` — the engine pushes on every hysteresis flip /
25349
+ * availability change; consumers never poll.
24673
25350
  */
24674
- /** Extensible condition tag. Seeded 'day' | 'night'; open by design so more can
24675
- * be added without a wire break (matching falls back to any-condition refs). */
25351
+ /** Extensible condition tag. Seeded 'day' | 'ir' (the two variants the operator
25352
+ * captures) plus 'night' | 'dawn' | 'dusk' from the resolver's sun-times band.
25353
+ * Open by design so more can be added without a wire break.
25354
+ *
25355
+ * Matching does NOT fall back across conditions: cross-condition cosines are
25356
+ * not comparable, so "I have never seen this scene in this light" is reported
25357
+ * as `unknown`, never guessed. A day reference scored against an IR frame
25358
+ * collapses the cosine and would latch a false alarm every single night. */
24676
25359
  var SceneConditionSchema = string();
25360
+ /**
25361
+ * What a scene does when the CURRENT light has no reference of its own.
25362
+ *
25363
+ * The lighting variants are not equally likely to exist. Almost every operator
25364
+ * captures daylight and then never stands outside at 22:00 to capture IR, and a
25365
+ * scene that is only ever going to be asked about a daytime question ("is the
25366
+ * bin still on the kerb at 08:00") does not need a night reference at all. The
25367
+ * night half must therefore be OPTIONAL, and optional means the scene keeps
25368
+ * working without it rather than degrading into a permanent complaint.
25369
+ *
25370
+ * - `skip` (default) — the check in that light is not made. Not a verdict, not
25371
+ * an alarm, not even an `unknown`: the live state simply stays whatever the
25372
+ * last covered light left it at, the latch is untouched, and the hysteresis
25373
+ * run is neither spent nor cleared. The scene resumes by itself at first
25374
+ * light. This is the only behaviour under which "I never captured IR" is a
25375
+ * configuration choice instead of a nightly fault.
25376
+ * - `judge-anyway` — score against the OTHER conditions' references. Available
25377
+ * for cameras whose IR frame is close enough to daylight (a floodlit
25378
+ * driveway, an always-white-light doorbell), and wrong for everything else:
25379
+ * cross-condition cosines are not comparable, so a day reference against a
25380
+ * true IR frame collapses and the scene reports a theft at 21:40.
25381
+ *
25382
+ * Never applies when the scene has NO comparable reference at all — that is
25383
+ * "not armed yet", it is reported as `no-reference-for-condition`, and silence
25384
+ * there would hide a scene the operator never finished setting up.
25385
+ */
25386
+ var SceneUncoveredPolicySchema = _enum(["skip", "judge-anyway"]);
25387
+ /** `matched` = the baseline is what we see; `diverged` = it demonstrably is not;
25388
+ * `unknown` = we cannot judge (no reference for this condition, encoder model
25389
+ * changed, view shifted, no snapshot). `unknown` is a real value, not a null,
25390
+ * and never counts toward hysteresis in either direction. */
25391
+ var SceneVerdictSchema = _enum([
25392
+ "matched",
25393
+ "diverged",
25394
+ "unknown"
25395
+ ]);
25396
+ /** Why a scene cannot judge. Named, because this feature's failure mode is
25397
+ * silence that reads as "nothing has happened". */
25398
+ var SceneUnavailableSchema = _enum([
25399
+ "no-reference-for-condition",
25400
+ "view-shifted",
25401
+ "no-vision-profile",
25402
+ "encoder-model-changed",
25403
+ "no-snapshot"
25404
+ ]);
24677
25405
  /** One captured reference — condition-tagged, model-version-gated. `embedding`
24678
25406
  * is `number[]` (Float32Array does NOT survive MsgPack/UDS). */
24679
25407
  var SceneReferenceSchema = object({
@@ -24681,7 +25409,14 @@ var SceneReferenceSchema = object({
24681
25409
  modelId: string(),
24682
25410
  condition: SceneConditionSchema,
24683
25411
  capturedAt: number(),
24684
- thumbnailMediaId: string().optional()
25412
+ thumbnailMediaId: string().optional(),
25413
+ /** Whole-frame (downscaled) embedding captured alongside the ROI crop. The
25414
+ * anti-view-shift anchor: a bumped camera, a PTZ preset or a re-aim makes the
25415
+ * normalized rect frame a different piece of world, and the scene would
25416
+ * diverge forever with a perfectly plausible cosine. Checked LAZILY, only
25417
+ * when hysteresis is about to flip — one extra encode per candidate
25418
+ * transition, not per poll. */
25419
+ anchorEmbedding: array(number()).optional()
24685
25420
  });
24686
25421
  var SceneMonitorStateSchema = object({
24687
25422
  id: string(),
@@ -24703,6 +25438,28 @@ var SceneCheckSchema = discriminatedUnion("mode", [object({
24703
25438
  profileId: string().optional(),
24704
25439
  hysteresisCount: number().int().positive()
24705
25440
  })]);
25441
+ var SCENE_DEFAULT_ANCHOR_THRESHOLD = .85;
25442
+ /** Night is OPTIONAL. A scene with only a daylight reference sits the IR hours
25443
+ * out in silence rather than reporting a fault every night. */
25444
+ var SCENE_DEFAULT_UNCOVERED_POLICY = "skip";
25445
+ /**
25446
+ * Vision-model adjudication of a candidate flip. Field names deliberately
25447
+ * mirror `NcConfirmSchema` so an operator meets one vocabulary, not two.
25448
+ *
25449
+ * `onTimeout` defaults to **'hold'**, the OPPOSITE of `NcConfirmGate`'s
25450
+ * fail-open: a notification suppressed is the worse error there, but a vision
25451
+ * model that timed out has not told us the bin is gone, and a latch is a
25452
+ * stateful claim that costs the operator a trip to reset.
25453
+ */
25454
+ var SceneConfirmSchema = object({
25455
+ enabled: boolean().default(false),
25456
+ prompt: string().min(1).max(1e3),
25457
+ profileId: string().optional(),
25458
+ timeoutMs: number().int().min(1e3).max(2e4).default(8e3),
25459
+ maxImagePx: number().int().min(64).max(2048).default(448),
25460
+ /** What a timeout / unavailable model means for the PENDING flip. */
25461
+ onTimeout: _enum(["flip", "hold"]).default("hold")
25462
+ });
24706
25463
  var SceneMonitorSchema = object({
24707
25464
  id: string(),
24708
25465
  label: string(),
@@ -24721,7 +25478,56 @@ var SceneMonitorSchema = object({
24721
25478
  lastConfidence: number().nullable(),
24722
25479
  currentCondition: SceneConditionSchema.nullable(),
24723
25480
  availability: _enum(["ok", "unavailable"]),
24724
- unavailableReason: string().nullable()
25481
+ unavailableReason: string().nullable(),
25482
+ /** Which state is "the initial screen". `null` until the first capture. */
25483
+ baselineStateId: string().nullable(),
25484
+ /** Which boolean drives notification rules and any export. */
25485
+ emit: _enum(["latched", "live"]).default("latched"),
25486
+ /** Live: does the region match the baseline RIGHT NOW. */
25487
+ verdict: SceneVerdictSchema,
25488
+ /** Has it been `diverged` at least once since `armedAt` — the operator's boolean. */
25489
+ latched: boolean(),
25490
+ /** Last reset (or creation). */
25491
+ armedAt: number(),
25492
+ divergedAt: number().nullable(),
25493
+ restoredAt: number().nullable(),
25494
+ /** A check is only COUNTED when the device has been quiet this long. Motion
25495
+ * during the window DISCARDS the observation — a car pulling up in front of
25496
+ * the bin must not be able to spend hysteresis credit. */
25497
+ quietSeconds: number().int().min(0).max(3600).default(60),
25498
+ /** An observation only advances the pending count when it is at least this
25499
+ * far from the previously counted one, so N agreeing checks span real time
25500
+ * rather than N adjacent polls inside one occlusion. */
25501
+ minObservationSpacingSec: number().int().min(0).max(3600).default(120),
25502
+ /** Vision-model adjudication of a candidate flip. Similarity primary only. */
25503
+ confirm: SceneConfirmSchema.optional(),
25504
+ /** Whole-frame anchor cosine below which a flip is REFUSED as `view-shifted`. */
25505
+ anchorThreshold: number().min(0).max(1).default(SCENE_DEFAULT_ANCHOR_THRESHOLD),
25506
+ /** Clear the latch on its own when the scene matches again? Default false —
25507
+ * `restoredAt` and the `scene-restored` edge are recorded regardless, so an
25508
+ * automation can react to the bin coming back without the operator's own
25509
+ * alarm silently clearing itself. */
25510
+ autoRestore: boolean().default(false),
25511
+ /** What to do when the current light has no reference of its own. See
25512
+ * {@link SceneUncoveredPolicySchema} — the default makes night OPTIONAL. */
25513
+ onUncoveredCondition: SceneUncoveredPolicySchema.default(SCENE_DEFAULT_UNCOVERED_POLICY),
25514
+ /**
25515
+ * The light whose checks are currently being SAT OUT under
25516
+ * `onUncoveredCondition: 'skip'` — `null` when the scene is checking normally.
25517
+ *
25518
+ * Engine-reported and advisory only: it moves no verdict, no latch and no
25519
+ * hysteresis. It exists so the card can say *"night (IR) — checks paused,
25520
+ * nothing captured in this light"* in the same calm voice as the coverage
25521
+ * line, because the alternative is a scene that silently stops answering
25522
+ * after sunset with nothing anywhere saying why. A skipped check must never
25523
+ * read as a broken one.
25524
+ */
25525
+ suspendedCondition: SceneConditionSchema.nullable().default(null),
25526
+ /** Named cause when `verdict === 'unknown'`. */
25527
+ unavailable: SceneUnavailableSchema.nullable(),
25528
+ /** Conditions that have at least one comparable reference — the coverage line
25529
+ * ("day ✓ · ir ✓ · dusk ✗") that turns a silent fallback into a visible fact. */
25530
+ coveredConditions: array(SceneConditionSchema)
24725
25531
  });
24726
25532
  var SceneMonitorStatusSchema = object({
24727
25533
  monitors: array(SceneMonitorSchema),
@@ -24754,7 +25560,15 @@ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSche
24754
25560
  "both"
24755
25561
  ]).optional(),
24756
25562
  checkIntervalSec: number().optional(),
24757
- check: SceneCheckSchema.optional()
25563
+ check: SceneCheckSchema.optional(),
25564
+ emit: _enum(["latched", "live"]).optional(),
25565
+ quietSeconds: number().int().min(0).max(3600).optional(),
25566
+ minObservationSpacingSec: number().int().min(0).max(3600).optional(),
25567
+ anchorThreshold: number().min(0).max(1).optional(),
25568
+ autoRestore: boolean().optional(),
25569
+ onUncoveredCondition: SceneUncoveredPolicySchema.optional(),
25570
+ /** `null` clears the vision-model adjudicator. */
25571
+ confirm: SceneConfirmSchema.nullable().optional()
24758
25572
  })
24759
25573
  }), _void(), {
24760
25574
  kind: "mutation",
@@ -24791,6 +25605,14 @@ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSche
24791
25605
  }), _void(), {
24792
25606
  kind: "mutation",
24793
25607
  auth: "admin"
25608
+ }), method(object({
25609
+ deviceId: number(),
25610
+ monitorId: string(),
25611
+ /** Defaults to TRUE at the provider seam — see `SCENE_RESET_RECAPTURES`. */
25612
+ recapture: boolean().optional()
25613
+ }), _void(), {
25614
+ kind: "mutation",
25615
+ auth: "admin"
24794
25616
  });
24795
25617
  /**
24796
25618
  * Per-stage gating mode applied to the zones a rule references.
@@ -24905,7 +25727,16 @@ DeviceType.Script, method(object({
24905
25727
  kind: "mutation",
24906
25728
  auth: "admin"
24907
25729
  });
24908
- object({
25730
+ /**
25731
+ * Smoke alarm sensor — boolean "is smoke currently detected" with
25732
+ * timestamp of the last transition. Drives Home Assistant
25733
+ * `binary_sensor` entries with `device_class: smoke`.
25734
+ *
25735
+ * Push-driven: a smoke event is critical, so the slice updates
25736
+ * immediately on the upstream signal. Auto-clearing back to false is
25737
+ * provider-controlled (some alarms latch until manually reset).
25738
+ */
25739
+ var SmokeStatusSchema = object({
24909
25740
  detected: boolean(),
24910
25741
  /** Ms epoch of the last transition. 0 if never observed. */
24911
25742
  lastChangedAt: number()
@@ -24944,6 +25775,16 @@ var CamStreamDescriptorSchema = object({
24944
25775
  /** Transport-specific opaque metadata (e.g. rfc4571 SDP). */
24945
25776
  metadata: record(string(), unknown()).optional()
24946
25777
  });
25778
+ object({
25779
+ /** The descriptors as last built from a real camera response. Never a guess:
25780
+ * a failed or refused build writes NOTHING, so a restored catalog is always
25781
+ * one the camera itself once produced. */
25782
+ descriptors: array(CamStreamDescriptorSchema),
25783
+ /** Ms epoch of the build that produced {@link descriptors}. Lets the wake
25784
+ * path decide whether the camera's own awake window is worth spending on a
25785
+ * re-read. */
25786
+ lastFetchedAt: number()
25787
+ });
24947
25788
  DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(CamStreamDescriptorSchema).readonly());
24948
25789
  /** One of the camera's stream profiles. */
24949
25790
  var StreamProfileSchema = _enum([
@@ -25099,12 +25940,64 @@ var NetworkAddressSchema = object({
25099
25940
  family: string(),
25100
25941
  internal: boolean()
25101
25942
  });
25943
+ /**
25944
+ * Provenance of the site coordinates, and the whole reason this is not just two
25945
+ * numbers.
25946
+ *
25947
+ * - `operator-set` — a human typed it, or accepted a detection. Authoritative;
25948
+ * nothing overwrites it.
25949
+ * - `derived-from-ip` — the hub geolocated its own public IP once, because a
25950
+ * default that is right to a few kilometres beats the coarse UTC clock split
25951
+ * the sun-times consumers otherwise fall back to.
25952
+ *
25953
+ * The UI shows which one it is. An operator who cannot tell a guess from their
25954
+ * own input will eventually trust the guess.
25955
+ */
25956
+ var SiteLocationSourceSchema = _enum(["operator-set", "derived-from-ip"]);
25957
+ /**
25958
+ * The read shape: the location plus the honest state of the one-shot derivation.
25959
+ *
25960
+ * `derivationAttemptedAt` is what makes the "one call, ever" contract
25961
+ * inspectable. When it is set and `location` is null, the geo-IP lookup ran and
25962
+ * failed; the hub will NOT try again on its own — the fallback is declared
25963
+ * (consumers degrade to their own last resort) and the operator either types the
25964
+ * coordinates or presses detect.
25965
+ */
25966
+ var SiteLocationStatusSchema = object({
25967
+ location: object({
25968
+ /** WGS84 decimal degrees. */
25969
+ latitude: number().min(-90).max(90),
25970
+ longitude: number().min(-180).max(180),
25971
+ source: SiteLocationSourceSchema,
25972
+ /** Epoch ms the value was last written. */
25973
+ updatedAt: number(),
25974
+ /**
25975
+ * Human-readable place the geo-IP service reported ("Napoli, IT"). Display
25976
+ * only — never parsed, never matched on. Absent for an operator-typed value.
25977
+ */
25978
+ label: string().optional()
25979
+ }).nullable(),
25980
+ derivationAttemptedAt: number().nullable(),
25981
+ /** Why the last derivation failed, for the UI to show instead of a shrug. */
25982
+ derivationError: string().nullable()
25983
+ });
25984
+ /** `null` clears the location and re-arms nothing — the derivation stays spent. */
25985
+ var SetSiteLocationInputSchema = object({
25986
+ latitude: number().min(-90).max(90),
25987
+ longitude: number().min(-180).max(180)
25988
+ }).nullable();
25102
25989
  method(_void(), FeatureManifestSchema), method(_void(), HealthStatusSchema), method(_void(), FeatureManifestSchema), method(_void(), array(NetworkAddressSchema).readonly()), method(_void(), unknown().nullable(), { auth: "admin" }), method(record(string(), unknown()), _null(), {
25103
25990
  kind: "mutation",
25104
25991
  auth: "admin"
25105
25992
  }), method(_void(), _void(), {
25106
25993
  kind: "mutation",
25107
25994
  auth: "admin"
25995
+ }), method(_void(), SiteLocationStatusSchema), method(SetSiteLocationInputSchema, SiteLocationStatusSchema, {
25996
+ kind: "mutation",
25997
+ auth: "admin"
25998
+ }), method(_void(), SiteLocationStatusSchema, {
25999
+ kind: "mutation",
26000
+ auth: "admin"
25108
26001
  });
25109
26002
  object({
25110
26003
  /** True when the device's tamper switch / case-open contact is
@@ -25114,7 +26007,23 @@ object({
25114
26007
  lastChangedAt: number()
25115
26008
  });
25116
26009
  DeviceType.Sensor;
25117
- object({
26010
+ /**
26011
+ * Single-metric temperature reading. Drives Home Assistant `sensor`
26012
+ * entries with `device_class: temperature` and any future native
26013
+ * thermometer.
26014
+ *
26015
+ * Unit normalisation: providers convert to Celsius before storing.
26016
+ * The slice value is always Celsius so cross-cap aggregators
26017
+ * (climate-control's `currentTemp`, energy analytics) can compose
26018
+ * without per-source unit fixups. The canonical display unit (`°C`) is
26019
+ * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
26020
+ * `sourceInfo`.
26021
+ *
26022
+ * Status `lastFetchedAt` lets staleness-aware consumers detect a
26023
+ * frozen feed (provider hung) distinct from a "temperature hasn't
26024
+ * changed" steady state.
26025
+ */
26026
+ var TemperatureSensorStatusSchema = object({
25118
26027
  /** Current temperature in Celsius. */
25119
26028
  celsius: number(),
25120
26029
  /** Ms epoch when the slice was last updated (push or poll). */
@@ -27834,6 +28743,12 @@ Object.freeze({
27834
28743
  addonId: null,
27835
28744
  access: "create"
27836
28745
  },
28746
+ "llm.cancel": {
28747
+ capName: "llm",
28748
+ capScope: "system",
28749
+ addonId: null,
28750
+ access: "create"
28751
+ },
27837
28752
  "llm.deleteModel": {
27838
28753
  capName: "llm",
27839
28754
  capScope: "system",
@@ -27918,6 +28833,12 @@ Object.freeze({
27918
28833
  addonId: null,
27919
28834
  access: "view"
27920
28835
  },
28836
+ "llm.resolveModelRef": {
28837
+ capName: "llm",
28838
+ capScope: "system",
28839
+ addonId: null,
28840
+ access: "create"
28841
+ },
27921
28842
  "llm.setDefault": {
27922
28843
  capName: "llm",
27923
28844
  capScope: "system",
@@ -30084,6 +31005,12 @@ Object.freeze({
30084
31005
  addonId: null,
30085
31006
  access: "create"
30086
31007
  },
31008
+ "sceneMonitor.resetScene": {
31009
+ capName: "scene-monitor",
31010
+ capScope: "device",
31011
+ addonId: null,
31012
+ access: "delete"
31013
+ },
30087
31014
  "sceneMonitor.updateScene": {
30088
31015
  capName: "scene-monitor",
30089
31016
  capScope: "device",
@@ -30762,6 +31689,12 @@ Object.freeze({
30762
31689
  addonId: null,
30763
31690
  access: "create"
30764
31691
  },
31692
+ "system.detectSiteLocation": {
31693
+ capName: "system",
31694
+ capScope: "system",
31695
+ addonId: null,
31696
+ access: "create"
31697
+ },
30765
31698
  "system.featureFlags": {
30766
31699
  capName: "system",
30767
31700
  capScope: "system",
@@ -30780,6 +31713,12 @@ Object.freeze({
30780
31713
  addonId: null,
30781
31714
  access: "view"
30782
31715
  },
31716
+ "system.getSiteLocation": {
31717
+ capName: "system",
31718
+ capScope: "system",
31719
+ addonId: null,
31720
+ access: "view"
31721
+ },
30783
31722
  "system.health": {
30784
31723
  capName: "system",
30785
31724
  capScope: "system",
@@ -30804,6 +31743,12 @@ Object.freeze({
30804
31743
  addonId: null,
30805
31744
  access: "create"
30806
31745
  },
31746
+ "system.setSiteLocation": {
31747
+ capName: "system",
31748
+ capScope: "system",
31749
+ addonId: null,
31750
+ access: "create"
31751
+ },
30807
31752
  "terminalSession.adoptLegacyMonitor": {
30808
31753
  capName: "terminal-session",
30809
31754
  capScope: "system",
@@ -31375,6 +32320,1704 @@ Object.freeze({
31375
32320
  access: "create"
31376
32321
  }
31377
32322
  });
32323
+ Object.freeze({
32324
+ "accessories.setChildHidden": [{
32325
+ name: "childDeviceId",
32326
+ form: "single",
32327
+ optional: false
32328
+ }, {
32329
+ name: "deviceId",
32330
+ form: "single",
32331
+ optional: false
32332
+ }],
32333
+ "addonSettings.getDeviceSettings": [{
32334
+ name: "deviceId",
32335
+ form: "single",
32336
+ optional: false
32337
+ }],
32338
+ "addonSettings.updateDeviceSettings": [{
32339
+ name: "deviceId",
32340
+ form: "single",
32341
+ optional: false
32342
+ }],
32343
+ "alarmPanel.arm": [{
32344
+ name: "deviceId",
32345
+ form: "single",
32346
+ optional: false
32347
+ }],
32348
+ "alarmPanel.disarm": [{
32349
+ name: "deviceId",
32350
+ form: "single",
32351
+ optional: false
32352
+ }],
32353
+ "alarmPanel.trigger": [{
32354
+ name: "deviceId",
32355
+ form: "single",
32356
+ optional: false
32357
+ }],
32358
+ "audioAnalysis.resolveDeviceSettings": [{
32359
+ name: "deviceId",
32360
+ form: "single",
32361
+ optional: false
32362
+ }],
32363
+ "audioAnalyzer.classify": [{
32364
+ name: "deviceId",
32365
+ form: "single",
32366
+ optional: true
32367
+ }],
32368
+ "audioMetrics.getCurrentSnapshot": [{
32369
+ name: "deviceId",
32370
+ form: "single",
32371
+ optional: false
32372
+ }],
32373
+ "audioMetrics.getHistory": [{
32374
+ name: "deviceId",
32375
+ form: "single",
32376
+ optional: false
32377
+ }],
32378
+ "automationControl.disable": [{
32379
+ name: "deviceId",
32380
+ form: "single",
32381
+ optional: false
32382
+ }],
32383
+ "automationControl.enable": [{
32384
+ name: "deviceId",
32385
+ form: "single",
32386
+ optional: false
32387
+ }],
32388
+ "automationControl.trigger": [{
32389
+ name: "deviceId",
32390
+ form: "single",
32391
+ optional: false
32392
+ }],
32393
+ "battery.wakeForStream": [{
32394
+ name: "deviceId",
32395
+ form: "single",
32396
+ optional: false
32397
+ }],
32398
+ "brightness.setBrightness": [{
32399
+ name: "deviceId",
32400
+ form: "single",
32401
+ optional: false
32402
+ }],
32403
+ "button.press": [{
32404
+ name: "deviceId",
32405
+ form: "single",
32406
+ optional: false
32407
+ }],
32408
+ "cameraCredentials.getCredentials": [{
32409
+ name: "deviceId",
32410
+ form: "single",
32411
+ optional: false
32412
+ }],
32413
+ "cameraStreams.getBrokerStreams": [{
32414
+ name: "deviceId",
32415
+ form: "single",
32416
+ optional: false
32417
+ }],
32418
+ "cameraStreams.getCameraStreams": [{
32419
+ name: "deviceId",
32420
+ form: "single",
32421
+ optional: false
32422
+ }],
32423
+ "cameraStreams.getProfileRtspEntries": [{
32424
+ name: "deviceId",
32425
+ form: "single",
32426
+ optional: false
32427
+ }],
32428
+ "cameraStreams.getRtspEntries": [{
32429
+ name: "deviceId",
32430
+ form: "single",
32431
+ optional: false
32432
+ }],
32433
+ "cameraStreams.pickStream": [{
32434
+ name: "deviceId",
32435
+ form: "single",
32436
+ optional: false
32437
+ }],
32438
+ "climateControl.setFanMode": [{
32439
+ name: "deviceId",
32440
+ form: "single",
32441
+ optional: false
32442
+ }],
32443
+ "climateControl.setMode": [{
32444
+ name: "deviceId",
32445
+ form: "single",
32446
+ optional: false
32447
+ }],
32448
+ "climateControl.setPreset": [{
32449
+ name: "deviceId",
32450
+ form: "single",
32451
+ optional: false
32452
+ }],
32453
+ "climateControl.setSwingHorizontal": [{
32454
+ name: "deviceId",
32455
+ form: "single",
32456
+ optional: false
32457
+ }],
32458
+ "climateControl.setSwingVertical": [{
32459
+ name: "deviceId",
32460
+ form: "single",
32461
+ optional: false
32462
+ }],
32463
+ "climateControl.setTarget": [{
32464
+ name: "deviceId",
32465
+ form: "single",
32466
+ optional: false
32467
+ }],
32468
+ "climateControl.setTargetHumidity": [{
32469
+ name: "deviceId",
32470
+ form: "single",
32471
+ optional: false
32472
+ }],
32473
+ "climateControl.setTargetRange": [{
32474
+ name: "deviceId",
32475
+ form: "single",
32476
+ optional: false
32477
+ }],
32478
+ "color.setColor": [{
32479
+ name: "deviceId",
32480
+ form: "single",
32481
+ optional: false
32482
+ }],
32483
+ "consumables.reset": [{
32484
+ name: "deviceId",
32485
+ form: "single",
32486
+ optional: false
32487
+ }],
32488
+ "control.setValue": [{
32489
+ name: "deviceId",
32490
+ form: "single",
32491
+ optional: false
32492
+ }],
32493
+ "cover.close": [{
32494
+ name: "deviceId",
32495
+ form: "single",
32496
+ optional: false
32497
+ }],
32498
+ "cover.open": [{
32499
+ name: "deviceId",
32500
+ form: "single",
32501
+ optional: false
32502
+ }],
32503
+ "cover.setPosition": [{
32504
+ name: "deviceId",
32505
+ form: "single",
32506
+ optional: false
32507
+ }],
32508
+ "cover.setTiltPosition": [{
32509
+ name: "deviceId",
32510
+ form: "single",
32511
+ optional: false
32512
+ }],
32513
+ "cover.stop": [{
32514
+ name: "deviceId",
32515
+ form: "single",
32516
+ optional: false
32517
+ }],
32518
+ "dayNight.getOptions": [{
32519
+ name: "deviceId",
32520
+ form: "single",
32521
+ optional: false
32522
+ }],
32523
+ "dayNight.setSettings": [{
32524
+ name: "deviceId",
32525
+ form: "single",
32526
+ optional: false
32527
+ }],
32528
+ "decoder.createSession": [{
32529
+ name: "deviceId",
32530
+ form: "single",
32531
+ optional: true
32532
+ }],
32533
+ "deviceAdoption.release": [{
32534
+ name: "camDeviceId",
32535
+ form: "single",
32536
+ optional: false
32537
+ }],
32538
+ "deviceAdoption.resync": [{
32539
+ name: "camDeviceId",
32540
+ form: "single",
32541
+ optional: false
32542
+ }],
32543
+ "deviceDiscovery.adoptDevice": [{
32544
+ name: "deviceId",
32545
+ form: "single",
32546
+ optional: false
32547
+ }],
32548
+ "deviceDiscovery.listDiscovered": [{
32549
+ name: "deviceId",
32550
+ form: "single",
32551
+ optional: false
32552
+ }],
32553
+ "deviceDiscovery.refreshDiscovery": [{
32554
+ name: "deviceId",
32555
+ form: "single",
32556
+ optional: false
32557
+ }],
32558
+ "deviceDiscovery.releaseDevice": [{
32559
+ name: "childDeviceId",
32560
+ form: "single",
32561
+ optional: false
32562
+ }, {
32563
+ name: "deviceId",
32564
+ form: "single",
32565
+ optional: false
32566
+ }],
32567
+ "deviceManager.adoptionRelease": [{
32568
+ name: "camDeviceId",
32569
+ form: "single",
32570
+ optional: false
32571
+ }],
32572
+ "deviceManager.adoptionResync": [{
32573
+ name: "camDeviceId",
32574
+ form: "single",
32575
+ optional: false
32576
+ }],
32577
+ "deviceManager.applyInitialMeta": [{
32578
+ name: "deviceId",
32579
+ form: "single",
32580
+ optional: false
32581
+ }, {
32582
+ name: "linkDeviceId",
32583
+ form: "single",
32584
+ optional: true
32585
+ }],
32586
+ "deviceManager.disable": [{
32587
+ name: "deviceId",
32588
+ form: "single",
32589
+ optional: false
32590
+ }],
32591
+ "deviceManager.enable": [{
32592
+ name: "deviceId",
32593
+ form: "single",
32594
+ optional: false
32595
+ }],
32596
+ "deviceManager.getBindings": [{
32597
+ name: "deviceId",
32598
+ form: "single",
32599
+ optional: false
32600
+ }],
32601
+ "deviceManager.getChildren": [{
32602
+ name: "parentDeviceId",
32603
+ form: "single",
32604
+ optional: false
32605
+ }],
32606
+ "deviceManager.getConfigSchema": [{
32607
+ name: "deviceId",
32608
+ form: "single",
32609
+ optional: false
32610
+ }],
32611
+ "deviceManager.getDevice": [{
32612
+ name: "deviceId",
32613
+ form: "single",
32614
+ optional: false
32615
+ }],
32616
+ "deviceManager.getDeviceAggregate": [{
32617
+ name: "deviceId",
32618
+ form: "single",
32619
+ optional: false
32620
+ }],
32621
+ "deviceManager.getDeviceLiveInfoAggregate": [{
32622
+ name: "deviceId",
32623
+ form: "single",
32624
+ optional: false
32625
+ }],
32626
+ "deviceManager.getDeviceSettingsAggregate": [{
32627
+ name: "deviceId",
32628
+ form: "single",
32629
+ optional: false
32630
+ }],
32631
+ "deviceManager.getDeviceStatusAggregate": [{
32632
+ name: "deviceId",
32633
+ form: "single",
32634
+ optional: false
32635
+ }],
32636
+ "deviceManager.getDeviceStatusAggregateBatch": [{
32637
+ name: "deviceIds",
32638
+ form: "array",
32639
+ optional: false
32640
+ }],
32641
+ "deviceManager.getLinkedDevices": [{
32642
+ name: "deviceId",
32643
+ form: "single",
32644
+ optional: false
32645
+ }],
32646
+ "deviceManager.getSettingsSchema": [{
32647
+ name: "deviceId",
32648
+ form: "single",
32649
+ optional: false
32650
+ }],
32651
+ "deviceManager.getStreamProfileMap": [{
32652
+ name: "deviceId",
32653
+ form: "single",
32654
+ optional: false
32655
+ }],
32656
+ "deviceManager.getStreamSources": [{
32657
+ name: "deviceId",
32658
+ form: "single",
32659
+ optional: false
32660
+ }],
32661
+ "deviceManager.getWireableFields": [{
32662
+ name: "deviceId",
32663
+ form: "single",
32664
+ optional: false
32665
+ }],
32666
+ "deviceManager.loadConfig": [{
32667
+ name: "deviceId",
32668
+ form: "single",
32669
+ optional: false
32670
+ }],
32671
+ "deviceManager.loadMeta": [{
32672
+ name: "deviceId",
32673
+ form: "single",
32674
+ optional: false
32675
+ }],
32676
+ "deviceManager.loadRuntimeState": [{
32677
+ name: "deviceId",
32678
+ form: "single",
32679
+ optional: false
32680
+ }],
32681
+ "deviceManager.persistConfig": [{
32682
+ name: "deviceId",
32683
+ form: "single",
32684
+ optional: false
32685
+ }],
32686
+ "deviceManager.probeStreams": [{
32687
+ name: "deviceId",
32688
+ form: "single",
32689
+ optional: false
32690
+ }],
32691
+ "deviceManager.registerDevice": [{
32692
+ name: "parentDeviceId",
32693
+ form: "single",
32694
+ optional: true
32695
+ }],
32696
+ "deviceManager.remove": [{
32697
+ name: "deviceId",
32698
+ form: "single",
32699
+ optional: false
32700
+ }],
32701
+ "deviceManager.removeDevice": [{
32702
+ name: "deviceId",
32703
+ form: "single",
32704
+ optional: false
32705
+ }],
32706
+ "deviceManager.runDeviceAction": [{
32707
+ name: "deviceId",
32708
+ form: "single",
32709
+ optional: false
32710
+ }],
32711
+ "deviceManager.setChildLayout": [{
32712
+ name: "deviceId",
32713
+ form: "single",
32714
+ optional: false
32715
+ }],
32716
+ "deviceManager.setDisabled": [{
32717
+ name: "deviceId",
32718
+ form: "single",
32719
+ optional: false
32720
+ }],
32721
+ "deviceManager.setDisplay": [{
32722
+ name: "deviceId",
32723
+ form: "single",
32724
+ optional: false
32725
+ }],
32726
+ "deviceManager.setIntegrationId": [{
32727
+ name: "deviceId",
32728
+ form: "single",
32729
+ optional: false
32730
+ }],
32731
+ "deviceManager.setLinkDeviceId": [{
32732
+ name: "deviceId",
32733
+ form: "single",
32734
+ optional: false
32735
+ }, {
32736
+ name: "linkDeviceId",
32737
+ form: "single",
32738
+ optional: true
32739
+ }],
32740
+ "deviceManager.setLocation": [{
32741
+ name: "deviceId",
32742
+ form: "single",
32743
+ optional: false
32744
+ }],
32745
+ "deviceManager.setMetadata": [{
32746
+ name: "deviceId",
32747
+ form: "single",
32748
+ optional: false
32749
+ }],
32750
+ "deviceManager.setName": [{
32751
+ name: "deviceId",
32752
+ form: "single",
32753
+ optional: false
32754
+ }],
32755
+ "deviceManager.setPrimaryChildEntityId": [{
32756
+ name: "deviceId",
32757
+ form: "single",
32758
+ optional: false
32759
+ }],
32760
+ "deviceManager.setRole": [{
32761
+ name: "deviceId",
32762
+ form: "single",
32763
+ optional: false
32764
+ }],
32765
+ "deviceManager.setStreamProfileMap": [{
32766
+ name: "deviceId",
32767
+ form: "single",
32768
+ optional: false
32769
+ }],
32770
+ "deviceManager.setType": [{
32771
+ name: "deviceId",
32772
+ form: "single",
32773
+ optional: false
32774
+ }],
32775
+ "deviceManager.setWrapperActive": [{
32776
+ name: "deviceId",
32777
+ form: "single",
32778
+ optional: false
32779
+ }],
32780
+ "deviceManager.testField": [{
32781
+ name: "deviceId",
32782
+ form: "single",
32783
+ optional: false
32784
+ }],
32785
+ "deviceManager.updateConfig": [{
32786
+ name: "deviceId",
32787
+ form: "single",
32788
+ optional: false
32789
+ }],
32790
+ "deviceManager.updateDeviceField": [{
32791
+ name: "deviceId",
32792
+ form: "single",
32793
+ optional: false
32794
+ }],
32795
+ "deviceManager.updateDeviceFieldsBatch": [{
32796
+ name: "deviceId",
32797
+ form: "single",
32798
+ optional: false
32799
+ }],
32800
+ "deviceOps.getConfigEntries": [{
32801
+ name: "deviceId",
32802
+ form: "single",
32803
+ optional: false
32804
+ }],
32805
+ "deviceOps.getRawState": [{
32806
+ name: "deviceId",
32807
+ form: "single",
32808
+ optional: false
32809
+ }],
32810
+ "deviceOps.getSettingsSchema": [{
32811
+ name: "deviceId",
32812
+ form: "single",
32813
+ optional: false
32814
+ }],
32815
+ "deviceOps.getStreamSources": [{
32816
+ name: "deviceId",
32817
+ form: "single",
32818
+ optional: false
32819
+ }],
32820
+ "deviceOps.removeDevice": [{
32821
+ name: "deviceId",
32822
+ form: "single",
32823
+ optional: false
32824
+ }],
32825
+ "deviceOps.runAction": [{
32826
+ name: "deviceId",
32827
+ form: "single",
32828
+ optional: false
32829
+ }],
32830
+ "deviceOps.setConfig": [{
32831
+ name: "deviceId",
32832
+ form: "single",
32833
+ optional: false
32834
+ }],
32835
+ "deviceState.getCapSlice": [{
32836
+ name: "deviceId",
32837
+ form: "single",
32838
+ optional: false
32839
+ }],
32840
+ "deviceState.getSnapshot": [{
32841
+ name: "deviceId",
32842
+ form: "single",
32843
+ optional: false
32844
+ }],
32845
+ "deviceState.setCapSlice": [{
32846
+ name: "deviceId",
32847
+ form: "single",
32848
+ optional: false
32849
+ }],
32850
+ "events.getEventClipUrl": [{
32851
+ name: "deviceId",
32852
+ form: "single",
32853
+ optional: false
32854
+ }],
32855
+ "events.getEvents": [{
32856
+ name: "deviceId",
32857
+ form: "single",
32858
+ optional: false
32859
+ }],
32860
+ "events.getEventThumbnail": [{
32861
+ name: "deviceId",
32862
+ form: "single",
32863
+ optional: false
32864
+ }],
32865
+ "faceGallery.getFaceByTrack": [{
32866
+ name: "deviceId",
32867
+ form: "single",
32868
+ optional: false
32869
+ }],
32870
+ "faceGallery.listRecentFaces": [{
32871
+ name: "deviceId",
32872
+ form: "single",
32873
+ optional: true
32874
+ }],
32875
+ "fanControl.setDirection": [{
32876
+ name: "deviceId",
32877
+ form: "single",
32878
+ optional: false
32879
+ }],
32880
+ "fanControl.setOscillating": [{
32881
+ name: "deviceId",
32882
+ form: "single",
32883
+ optional: false
32884
+ }],
32885
+ "fanControl.setPercentage": [{
32886
+ name: "deviceId",
32887
+ form: "single",
32888
+ optional: false
32889
+ }],
32890
+ "fanControl.setPreset": [{
32891
+ name: "deviceId",
32892
+ form: "single",
32893
+ optional: false
32894
+ }],
32895
+ "humidifier.setMode": [{
32896
+ name: "deviceId",
32897
+ form: "single",
32898
+ optional: false
32899
+ }],
32900
+ "humidifier.setOn": [{
32901
+ name: "deviceId",
32902
+ form: "single",
32903
+ optional: false
32904
+ }],
32905
+ "humidifier.setTargetHumidity": [{
32906
+ name: "deviceId",
32907
+ form: "single",
32908
+ optional: false
32909
+ }],
32910
+ "imageSettings.getOptions": [{
32911
+ name: "deviceId",
32912
+ form: "single",
32913
+ optional: false
32914
+ }],
32915
+ "imageSettings.setSettings": [{
32916
+ name: "deviceId",
32917
+ form: "single",
32918
+ optional: false
32919
+ }],
32920
+ "intercom.endTalkSession": [{
32921
+ name: "deviceId",
32922
+ form: "single",
32923
+ optional: false
32924
+ }],
32925
+ "intercom.handleAnswer": [{
32926
+ name: "deviceId",
32927
+ form: "single",
32928
+ optional: false
32929
+ }],
32930
+ "intercom.pushTalkAudio": [{
32931
+ name: "deviceId",
32932
+ form: "single",
32933
+ optional: false
32934
+ }],
32935
+ "intercom.startSession": [{
32936
+ name: "deviceId",
32937
+ form: "single",
32938
+ optional: false
32939
+ }],
32940
+ "intercom.startTalkSession": [{
32941
+ name: "deviceId",
32942
+ form: "single",
32943
+ optional: false
32944
+ }],
32945
+ "intercom.stopSession": [{
32946
+ name: "deviceId",
32947
+ form: "single",
32948
+ optional: false
32949
+ }],
32950
+ "lawnMowerControl.dock": [{
32951
+ name: "deviceId",
32952
+ form: "single",
32953
+ optional: false
32954
+ }],
32955
+ "lawnMowerControl.pause": [{
32956
+ name: "deviceId",
32957
+ form: "single",
32958
+ optional: false
32959
+ }],
32960
+ "lawnMowerControl.startMowing": [{
32961
+ name: "deviceId",
32962
+ form: "single",
32963
+ optional: false
32964
+ }],
32965
+ "lockControl.lock": [{
32966
+ name: "deviceId",
32967
+ form: "single",
32968
+ optional: false
32969
+ }],
32970
+ "lockControl.open": [{
32971
+ name: "deviceId",
32972
+ form: "single",
32973
+ optional: false
32974
+ }],
32975
+ "lockControl.unlock": [{
32976
+ name: "deviceId",
32977
+ form: "single",
32978
+ optional: false
32979
+ }],
32980
+ "mediaPlayer.next": [{
32981
+ name: "deviceId",
32982
+ form: "single",
32983
+ optional: false
32984
+ }],
32985
+ "mediaPlayer.pause": [{
32986
+ name: "deviceId",
32987
+ form: "single",
32988
+ optional: false
32989
+ }],
32990
+ "mediaPlayer.play": [{
32991
+ name: "deviceId",
32992
+ form: "single",
32993
+ optional: false
32994
+ }],
32995
+ "mediaPlayer.playMedia": [{
32996
+ name: "deviceId",
32997
+ form: "single",
32998
+ optional: false
32999
+ }],
33000
+ "mediaPlayer.previous": [{
33001
+ name: "deviceId",
33002
+ form: "single",
33003
+ optional: false
33004
+ }],
33005
+ "mediaPlayer.seek": [{
33006
+ name: "deviceId",
33007
+ form: "single",
33008
+ optional: false
33009
+ }],
33010
+ "mediaPlayer.selectSource": [{
33011
+ name: "deviceId",
33012
+ form: "single",
33013
+ optional: false
33014
+ }],
33015
+ "mediaPlayer.setMute": [{
33016
+ name: "deviceId",
33017
+ form: "single",
33018
+ optional: false
33019
+ }],
33020
+ "mediaPlayer.setRepeat": [{
33021
+ name: "deviceId",
33022
+ form: "single",
33023
+ optional: false
33024
+ }],
33025
+ "mediaPlayer.setShuffle": [{
33026
+ name: "deviceId",
33027
+ form: "single",
33028
+ optional: false
33029
+ }],
33030
+ "mediaPlayer.setVolume": [{
33031
+ name: "deviceId",
33032
+ form: "single",
33033
+ optional: false
33034
+ }],
33035
+ "mediaPlayer.stop": [{
33036
+ name: "deviceId",
33037
+ form: "single",
33038
+ optional: false
33039
+ }],
33040
+ "motion.isDetected": [{
33041
+ name: "deviceId",
33042
+ form: "single",
33043
+ optional: false
33044
+ }],
33045
+ "motionDetection.analyze": [{
33046
+ name: "deviceId",
33047
+ form: "single",
33048
+ optional: false
33049
+ }],
33050
+ "motionDetection.removeCamera": [{
33051
+ name: "deviceId",
33052
+ form: "single",
33053
+ optional: false
33054
+ }],
33055
+ "motionTrigger.setMotionTrigger": [{
33056
+ name: "deviceId",
33057
+ form: "single",
33058
+ optional: false
33059
+ }],
33060
+ "motionZones.getOptions": [{
33061
+ name: "deviceId",
33062
+ form: "single",
33063
+ optional: false
33064
+ }],
33065
+ "motionZones.setZone": [{
33066
+ name: "deviceId",
33067
+ form: "single",
33068
+ optional: false
33069
+ }],
33070
+ "nativeObjectDetection.setEnabled": [{
33071
+ name: "deviceId",
33072
+ form: "single",
33073
+ optional: false
33074
+ }],
33075
+ "networkQuality.getDeviceStats": [{
33076
+ name: "deviceId",
33077
+ form: "single",
33078
+ optional: false
33079
+ }],
33080
+ "networkQuality.reportClientStats": [{
33081
+ name: "deviceId",
33082
+ form: "single",
33083
+ optional: false
33084
+ }],
33085
+ "notificationRules.setDeviceMuted": [{
33086
+ name: "deviceId",
33087
+ form: "single",
33088
+ optional: false
33089
+ }],
33090
+ "notifier.cancel": [{
33091
+ name: "deviceId",
33092
+ form: "single",
33093
+ optional: false
33094
+ }],
33095
+ "notifier.send": [{
33096
+ name: "deviceId",
33097
+ form: "single",
33098
+ optional: false
33099
+ }],
33100
+ "osd.setOverlay": [{
33101
+ name: "deviceId",
33102
+ form: "single",
33103
+ optional: false
33104
+ }],
33105
+ "osdManager.clearSlotBinding": [{
33106
+ name: "deviceId",
33107
+ form: "single",
33108
+ optional: false
33109
+ }],
33110
+ "osdManager.copyDeviceConfiguration": [{
33111
+ name: "sourceDeviceId",
33112
+ form: "single",
33113
+ optional: false
33114
+ }, {
33115
+ name: "targetDeviceId",
33116
+ form: "single",
33117
+ optional: false
33118
+ }],
33119
+ "osdManager.getDeviceOsd": [{
33120
+ name: "deviceId",
33121
+ form: "single",
33122
+ optional: false
33123
+ }],
33124
+ "osdManager.getSourceCatalog": [{
33125
+ name: "deviceId",
33126
+ form: "single",
33127
+ optional: false
33128
+ }],
33129
+ "osdManager.previewSlot": [{
33130
+ name: "deviceId",
33131
+ form: "single",
33132
+ optional: false
33133
+ }],
33134
+ "osdManager.renderDevice": [{
33135
+ name: "deviceId",
33136
+ form: "single",
33137
+ optional: false
33138
+ }],
33139
+ "osdManager.setSlotBinding": [{
33140
+ name: "deviceId",
33141
+ form: "single",
33142
+ optional: false
33143
+ }],
33144
+ "petFeeder.callPet": [{
33145
+ name: "deviceId",
33146
+ form: "single",
33147
+ optional: false
33148
+ }],
33149
+ "petFeeder.cancelFeed": [{
33150
+ name: "deviceId",
33151
+ form: "single",
33152
+ optional: false
33153
+ }],
33154
+ "petFeeder.feed": [{
33155
+ name: "deviceId",
33156
+ form: "single",
33157
+ optional: false
33158
+ }],
33159
+ "petFeeder.markFoodReplenished": [{
33160
+ name: "deviceId",
33161
+ form: "single",
33162
+ optional: false
33163
+ }],
33164
+ "petFeeder.playSound": [{
33165
+ name: "deviceId",
33166
+ form: "single",
33167
+ optional: false
33168
+ }],
33169
+ "petFeeder.resetDesiccant": [{
33170
+ name: "deviceId",
33171
+ form: "single",
33172
+ optional: false
33173
+ }],
33174
+ "petFeeder.setChildLock": [{
33175
+ name: "deviceId",
33176
+ form: "single",
33177
+ optional: false
33178
+ }],
33179
+ "petFeeder.setFeedSound": [{
33180
+ name: "deviceId",
33181
+ form: "single",
33182
+ optional: false
33183
+ }],
33184
+ "petFeeder.setIndicatorLight": [{
33185
+ name: "deviceId",
33186
+ form: "single",
33187
+ optional: false
33188
+ }],
33189
+ "petFeeder.setVolume": [{
33190
+ name: "deviceId",
33191
+ form: "single",
33192
+ optional: false
33193
+ }],
33194
+ "pipelineAnalytics.clearTracks": [{
33195
+ name: "deviceId",
33196
+ form: "single",
33197
+ optional: false
33198
+ }],
33199
+ "pipelineAnalytics.completeRetrainTrack": [{
33200
+ name: "deviceId",
33201
+ form: "single",
33202
+ optional: false
33203
+ }],
33204
+ "pipelineAnalytics.deleteDeviceEvents": [{
33205
+ name: "deviceId",
33206
+ form: "single",
33207
+ optional: false
33208
+ }],
33209
+ "pipelineAnalytics.deleteTracks": [{
33210
+ name: "deviceId",
33211
+ form: "single",
33212
+ optional: false
33213
+ }],
33214
+ "pipelineAnalytics.deselectRetrainFrame": [{
33215
+ name: "deviceId",
33216
+ form: "single",
33217
+ optional: false
33218
+ }],
33219
+ "pipelineAnalytics.getActiveTracks": [{
33220
+ name: "deviceId",
33221
+ form: "single",
33222
+ optional: false
33223
+ }],
33224
+ "pipelineAnalytics.getAudioEvents": [{
33225
+ name: "deviceId",
33226
+ form: "single",
33227
+ optional: false
33228
+ }],
33229
+ "pipelineAnalytics.getEventDensity": [{
33230
+ name: "deviceId",
33231
+ form: "single",
33232
+ optional: false
33233
+ }],
33234
+ "pipelineAnalytics.getEventMedia": [{
33235
+ name: "deviceId",
33236
+ form: "single",
33237
+ optional: false
33238
+ }],
33239
+ "pipelineAnalytics.getKeyEvents": [{
33240
+ name: "deviceId",
33241
+ form: "single",
33242
+ optional: false
33243
+ }],
33244
+ "pipelineAnalytics.getMotionEvents": [{
33245
+ name: "deviceId",
33246
+ form: "single",
33247
+ optional: false
33248
+ }],
33249
+ "pipelineAnalytics.getObjectEvents": [{
33250
+ name: "deviceId",
33251
+ form: "single",
33252
+ optional: false
33253
+ }],
33254
+ "pipelineAnalytics.getRetrainExportUrl": [{
33255
+ name: "deviceIds",
33256
+ form: "array",
33257
+ optional: true
33258
+ }],
33259
+ "pipelineAnalytics.getSensorEvents": [{
33260
+ name: "deviceId",
33261
+ form: "single",
33262
+ optional: false
33263
+ }],
33264
+ "pipelineAnalytics.getTrack": [{
33265
+ name: "deviceId",
33266
+ form: "single",
33267
+ optional: false
33268
+ }],
33269
+ "pipelineAnalytics.getTrackMedia": [{
33270
+ name: "deviceId",
33271
+ form: "single",
33272
+ optional: false
33273
+ }],
33274
+ "pipelineAnalytics.getTrainingExportSummary": [{
33275
+ name: "deviceIds",
33276
+ form: "array",
33277
+ optional: true
33278
+ }],
33279
+ "pipelineAnalytics.getTrainingExportUrl": [{
33280
+ name: "deviceIds",
33281
+ form: "array",
33282
+ optional: true
33283
+ }],
33284
+ "pipelineAnalytics.listEventKinds": [{
33285
+ name: "deviceId",
33286
+ form: "single",
33287
+ optional: false
33288
+ }],
33289
+ "pipelineAnalytics.listEventKindsBatch": [{
33290
+ name: "deviceIds",
33291
+ form: "array",
33292
+ optional: false
33293
+ }],
33294
+ "pipelineAnalytics.listOpsLog": [{
33295
+ name: "deviceId",
33296
+ form: "single",
33297
+ optional: true
33298
+ }],
33299
+ "pipelineAnalytics.listRecentTracks": [{
33300
+ name: "deviceIds",
33301
+ form: "array",
33302
+ optional: false
33303
+ }],
33304
+ "pipelineAnalytics.listRetrainStaging": [{
33305
+ name: "deviceIds",
33306
+ form: "array",
33307
+ optional: true
33308
+ }],
33309
+ "pipelineAnalytics.listTrackMedia": [{
33310
+ name: "deviceId",
33311
+ form: "single",
33312
+ optional: false
33313
+ }],
33314
+ "pipelineAnalytics.listTracks": [{
33315
+ name: "deviceId",
33316
+ form: "single",
33317
+ optional: false
33318
+ }],
33319
+ "pipelineAnalytics.proposeRetrainAnnotations": [{
33320
+ name: "deviceId",
33321
+ form: "single",
33322
+ optional: false
33323
+ }],
33324
+ "pipelineAnalytics.pruneEventsBefore": [{
33325
+ name: "deviceId",
33326
+ form: "single",
33327
+ optional: false
33328
+ }],
33329
+ "pipelineAnalytics.pruneTracksBefore": [{
33330
+ name: "deviceId",
33331
+ form: "single",
33332
+ optional: false
33333
+ }],
33334
+ "pipelineAnalytics.rebuildObjectEmbeddings": [{
33335
+ name: "deviceId",
33336
+ form: "single",
33337
+ optional: true
33338
+ }],
33339
+ "pipelineAnalytics.restageRetrainTrack": [{
33340
+ name: "deviceId",
33341
+ form: "single",
33342
+ optional: false
33343
+ }],
33344
+ "pipelineAnalytics.saveRetrainAnnotations": [{
33345
+ name: "deviceId",
33346
+ form: "single",
33347
+ optional: false
33348
+ }],
33349
+ "pipelineAnalytics.searchObjectEvents": [{
33350
+ name: "deviceId",
33351
+ form: "single",
33352
+ optional: true
33353
+ }],
33354
+ "pipelineAnalytics.selectRetrainFrames": [{
33355
+ name: "deviceId",
33356
+ form: "single",
33357
+ optional: false
33358
+ }],
33359
+ "pipelineAnalytics.setTrackFlags": [{
33360
+ name: "deviceId",
33361
+ form: "single",
33362
+ optional: false
33363
+ }],
33364
+ "pipelineAnalytics.wipeAllAnalytics": [{
33365
+ name: "deviceId",
33366
+ form: "single",
33367
+ optional: false
33368
+ }],
33369
+ "pipelineExecutor.runPipeline": [{
33370
+ name: "deviceId",
33371
+ form: "single",
33372
+ optional: true
33373
+ }],
33374
+ "pipelineExecutor.runPipelineBatch": [{
33375
+ name: "deviceId",
33376
+ form: "single",
33377
+ optional: true
33378
+ }],
33379
+ "pipelineOrchestrator.assignAudio": [{
33380
+ name: "deviceId",
33381
+ form: "single",
33382
+ optional: false
33383
+ }],
33384
+ "pipelineOrchestrator.assignPipeline": [{
33385
+ name: "deviceId",
33386
+ form: "single",
33387
+ optional: false
33388
+ }],
33389
+ "pipelineOrchestrator.getAudioAssignment": [{
33390
+ name: "deviceId",
33391
+ form: "single",
33392
+ optional: false
33393
+ }],
33394
+ "pipelineOrchestrator.getCameraMetrics": [{
33395
+ name: "deviceId",
33396
+ form: "single",
33397
+ optional: false
33398
+ }],
33399
+ "pipelineOrchestrator.getCameraSettings": [{
33400
+ name: "deviceId",
33401
+ form: "single",
33402
+ optional: false
33403
+ }],
33404
+ "pipelineOrchestrator.getCameraStatus": [{
33405
+ name: "deviceId",
33406
+ form: "single",
33407
+ optional: false
33408
+ }],
33409
+ "pipelineOrchestrator.getCameraStatuses": [{
33410
+ name: "deviceIds",
33411
+ form: "array",
33412
+ optional: true
33413
+ }],
33414
+ "pipelineOrchestrator.getCameraStepOverrides": [{
33415
+ name: "deviceId",
33416
+ form: "single",
33417
+ optional: false
33418
+ }],
33419
+ "pipelineOrchestrator.getCameraSwitches": [{
33420
+ name: "deviceId",
33421
+ form: "single",
33422
+ optional: false
33423
+ }],
33424
+ "pipelineOrchestrator.getPipelineAssignment": [{
33425
+ name: "deviceId",
33426
+ form: "single",
33427
+ optional: false
33428
+ }],
33429
+ "pipelineOrchestrator.getPipelineDevicePin": [{
33430
+ name: "deviceId",
33431
+ form: "single",
33432
+ optional: false
33433
+ }],
33434
+ "pipelineOrchestrator.resolvePipeline": [{
33435
+ name: "deviceId",
33436
+ form: "single",
33437
+ optional: false
33438
+ }],
33439
+ "pipelineOrchestrator.setCameraPipelineForAgent": [{
33440
+ name: "deviceId",
33441
+ form: "single",
33442
+ optional: false
33443
+ }],
33444
+ "pipelineOrchestrator.setCameraStepOverride": [{
33445
+ name: "deviceId",
33446
+ form: "single",
33447
+ optional: false
33448
+ }],
33449
+ "pipelineOrchestrator.setCameraStepToggle": [{
33450
+ name: "deviceId",
33451
+ form: "single",
33452
+ optional: false
33453
+ }],
33454
+ "pipelineOrchestrator.setCameraSwitch": [{
33455
+ name: "deviceId",
33456
+ form: "single",
33457
+ optional: false
33458
+ }],
33459
+ "pipelineOrchestrator.setPipelineDevicePin": [{
33460
+ name: "deviceId",
33461
+ form: "single",
33462
+ optional: false
33463
+ }],
33464
+ "pipelineOrchestrator.unassignAudio": [{
33465
+ name: "deviceId",
33466
+ form: "single",
33467
+ optional: false
33468
+ }],
33469
+ "pipelineOrchestrator.unassignPipeline": [{
33470
+ name: "deviceId",
33471
+ form: "single",
33472
+ optional: false
33473
+ }],
33474
+ "pipelineRunner.attachCamera": [{
33475
+ name: "deviceId",
33476
+ form: "single",
33477
+ optional: false
33478
+ }],
33479
+ "pipelineRunner.detachCamera": [{
33480
+ name: "deviceId",
33481
+ form: "single",
33482
+ optional: false
33483
+ }],
33484
+ "pipelineRunner.getCameraMetrics": [{
33485
+ name: "deviceId",
33486
+ form: "single",
33487
+ optional: false
33488
+ }],
33489
+ "pipelineRunner.reportMotion": [{
33490
+ name: "deviceId",
33491
+ form: "single",
33492
+ optional: false
33493
+ }],
33494
+ "pipelineRunner.runDetailSubtree": [{
33495
+ name: "deviceId",
33496
+ form: "single",
33497
+ optional: false
33498
+ }],
33499
+ "pipelineRunner.runStatelessStep": [{
33500
+ name: "sourceDeviceId",
33501
+ form: "single",
33502
+ optional: false
33503
+ }],
33504
+ "plateGallery.getPlateByTrack": [{
33505
+ name: "deviceId",
33506
+ form: "single",
33507
+ optional: false
33508
+ }],
33509
+ "plateGallery.listPlates": [{
33510
+ name: "deviceId",
33511
+ form: "single",
33512
+ optional: true
33513
+ }],
33514
+ "privacyMask.getOptions": [{
33515
+ name: "deviceId",
33516
+ form: "single",
33517
+ optional: false
33518
+ }],
33519
+ "privacyMask.setAudioEnabled": [{
33520
+ name: "deviceId",
33521
+ form: "single",
33522
+ optional: false
33523
+ }],
33524
+ "privacyMask.setMask": [{
33525
+ name: "deviceId",
33526
+ form: "single",
33527
+ optional: false
33528
+ }],
33529
+ "ptz.continuousMove": [{
33530
+ name: "deviceId",
33531
+ form: "single",
33532
+ optional: false
33533
+ }],
33534
+ "ptz.deletePreset": [{
33535
+ name: "deviceId",
33536
+ form: "single",
33537
+ optional: false
33538
+ }],
33539
+ "ptz.getOptions": [{
33540
+ name: "deviceId",
33541
+ form: "single",
33542
+ optional: false
33543
+ }],
33544
+ "ptz.getPosition": [{
33545
+ name: "deviceId",
33546
+ form: "single",
33547
+ optional: false
33548
+ }],
33549
+ "ptz.getPresets": [{
33550
+ name: "deviceId",
33551
+ form: "single",
33552
+ optional: false
33553
+ }],
33554
+ "ptz.goHome": [{
33555
+ name: "deviceId",
33556
+ form: "single",
33557
+ optional: false
33558
+ }],
33559
+ "ptz.goToPreset": [{
33560
+ name: "deviceId",
33561
+ form: "single",
33562
+ optional: false
33563
+ }],
33564
+ "ptz.move": [{
33565
+ name: "deviceId",
33566
+ form: "single",
33567
+ optional: false
33568
+ }],
33569
+ "ptz.savePreset": [{
33570
+ name: "deviceId",
33571
+ form: "single",
33572
+ optional: false
33573
+ }],
33574
+ "ptz.setAutofocus": [{
33575
+ name: "deviceId",
33576
+ form: "single",
33577
+ optional: false
33578
+ }],
33579
+ "ptz.stop": [{
33580
+ name: "deviceId",
33581
+ form: "single",
33582
+ optional: false
33583
+ }],
33584
+ "ptzAutotrack.getSettings": [{
33585
+ name: "deviceId",
33586
+ form: "single",
33587
+ optional: false
33588
+ }],
33589
+ "ptzAutotrack.getStatus": [{
33590
+ name: "deviceId",
33591
+ form: "single",
33592
+ optional: false
33593
+ }],
33594
+ "ptzAutotrack.setEnabled": [{
33595
+ name: "deviceId",
33596
+ form: "single",
33597
+ optional: false
33598
+ }],
33599
+ "ptzAutotrack.setSettings": [{
33600
+ name: "deviceId",
33601
+ form: "single",
33602
+ optional: false
33603
+ }],
33604
+ "reboot.reboot": [{
33605
+ name: "deviceId",
33606
+ form: "single",
33607
+ optional: false
33608
+ }],
33609
+ "recording.deleteFootprint": [{
33610
+ name: "deviceId",
33611
+ form: "single",
33612
+ optional: false
33613
+ }],
33614
+ "recording.getAvailability": [{
33615
+ name: "deviceId",
33616
+ form: "single",
33617
+ optional: false
33618
+ }],
33619
+ "recording.getDaysWithRecordings": [{
33620
+ name: "deviceId",
33621
+ form: "single",
33622
+ optional: false
33623
+ }],
33624
+ "recording.getDeviceConfig": [{
33625
+ name: "deviceId",
33626
+ form: "single",
33627
+ optional: false
33628
+ }],
33629
+ "recording.getPlaybackManifest": [{
33630
+ name: "deviceId",
33631
+ form: "single",
33632
+ optional: false
33633
+ }],
33634
+ "recording.listOpsLog": [{
33635
+ name: "deviceId",
33636
+ form: "single",
33637
+ optional: true
33638
+ }],
33639
+ "recording.locateSegment": [{
33640
+ name: "deviceId",
33641
+ form: "single",
33642
+ optional: false
33643
+ }],
33644
+ "recording.pruneFootage": [{
33645
+ name: "deviceId",
33646
+ form: "single",
33647
+ optional: false
33648
+ }],
33649
+ "recording.readGopBytes": [{
33650
+ name: "deviceId",
33651
+ form: "single",
33652
+ optional: false
33653
+ }],
33654
+ "recording.readSegmentBytes": [{
33655
+ name: "deviceId",
33656
+ form: "single",
33657
+ optional: false
33658
+ }],
33659
+ "recording.relocateFootage": [{
33660
+ name: "deviceId",
33661
+ form: "single",
33662
+ optional: true
33663
+ }],
33664
+ "recording.renderClip": [{
33665
+ name: "deviceId",
33666
+ form: "single",
33667
+ optional: false
33668
+ }],
33669
+ "recording.renderGif": [{
33670
+ name: "deviceId",
33671
+ form: "single",
33672
+ optional: false
33673
+ }],
33674
+ "recording.rescanStorage": [{
33675
+ name: "deviceId",
33676
+ form: "single",
33677
+ optional: false
33678
+ }],
33679
+ "recording.setDeviceConfig": [{
33680
+ name: "deviceId",
33681
+ form: "single",
33682
+ optional: false
33683
+ }],
33684
+ "recording.startStorageMigrationMove": [{
33685
+ name: "deviceId",
33686
+ form: "single",
33687
+ optional: true
33688
+ }],
33689
+ "recordingExport.createExport": [{
33690
+ name: "deviceId",
33691
+ form: "single",
33692
+ optional: false
33693
+ }],
33694
+ "recordingExport.listExports": [{
33695
+ name: "deviceId",
33696
+ form: "single",
33697
+ optional: true
33698
+ }],
33699
+ "sceneMonitor.captureReference": [{
33700
+ name: "deviceId",
33701
+ form: "single",
33702
+ optional: false
33703
+ }],
33704
+ "sceneMonitor.createScene": [{
33705
+ name: "deviceId",
33706
+ form: "single",
33707
+ optional: false
33708
+ }],
33709
+ "sceneMonitor.deleteReference": [{
33710
+ name: "deviceId",
33711
+ form: "single",
33712
+ optional: false
33713
+ }],
33714
+ "sceneMonitor.deleteScene": [{
33715
+ name: "deviceId",
33716
+ form: "single",
33717
+ optional: false
33718
+ }],
33719
+ "sceneMonitor.listScenes": [{
33720
+ name: "deviceId",
33721
+ form: "single",
33722
+ optional: false
33723
+ }],
33724
+ "sceneMonitor.recheckNow": [{
33725
+ name: "deviceId",
33726
+ form: "single",
33727
+ optional: false
33728
+ }],
33729
+ "sceneMonitor.resetScene": [{
33730
+ name: "deviceId",
33731
+ form: "single",
33732
+ optional: false
33733
+ }],
33734
+ "sceneMonitor.updateScene": [{
33735
+ name: "deviceId",
33736
+ form: "single",
33737
+ optional: false
33738
+ }],
33739
+ "scriptRunner.run": [{
33740
+ name: "deviceId",
33741
+ form: "single",
33742
+ optional: false
33743
+ }],
33744
+ "scriptRunner.stop": [{
33745
+ name: "deviceId",
33746
+ form: "single",
33747
+ optional: false
33748
+ }],
33749
+ "snapshot.getSnapshot": [{
33750
+ name: "deviceId",
33751
+ form: "single",
33752
+ optional: false
33753
+ }],
33754
+ "snapshot.getSnapshotLinks": [{
33755
+ name: "targets",
33756
+ form: "object-array",
33757
+ optional: false,
33758
+ itemField: "deviceId"
33759
+ }],
33760
+ "snapshot.getSnapshotOverview": [{
33761
+ name: "deviceIds",
33762
+ form: "array",
33763
+ optional: false
33764
+ }],
33765
+ "snapshot.invalidateCache": [{
33766
+ name: "deviceId",
33767
+ form: "single",
33768
+ optional: false
33769
+ }],
33770
+ "streamBroker.acquireEgressTranscode": [{
33771
+ name: "deviceId",
33772
+ form: "single",
33773
+ optional: false
33774
+ }],
33775
+ "streamBroker.assignProfile": [{
33776
+ name: "deviceId",
33777
+ form: "single",
33778
+ optional: false
33779
+ }],
33780
+ "streamBroker.getDeviceAudioMute": [{
33781
+ name: "deviceId",
33782
+ form: "single",
33783
+ optional: false
33784
+ }],
33785
+ "streamBroker.getStreamWithCodec": [{
33786
+ name: "deviceId",
33787
+ form: "single",
33788
+ optional: false
33789
+ }],
33790
+ "streamBroker.produceEventMedia": [{
33791
+ name: "deviceId",
33792
+ form: "single",
33793
+ optional: false
33794
+ }],
33795
+ "streamBroker.publishCameraStream": [{
33796
+ name: "deviceId",
33797
+ form: "single",
33798
+ optional: false
33799
+ }],
33800
+ "streamBroker.renderPreBufferClip": [{
33801
+ name: "deviceId",
33802
+ form: "single",
33803
+ optional: false
33804
+ }],
33805
+ "streamBroker.restartProfile": [{
33806
+ name: "deviceId",
33807
+ form: "single",
33808
+ optional: false
33809
+ }],
33810
+ "streamBroker.retractCameraStream": [{
33811
+ name: "deviceId",
33812
+ form: "single",
33813
+ optional: false
33814
+ }],
33815
+ "streamBroker.setDeviceAudioMute": [{
33816
+ name: "deviceId",
33817
+ form: "single",
33818
+ optional: false
33819
+ }],
33820
+ "streamBroker.unassignProfile": [{
33821
+ name: "deviceId",
33822
+ form: "single",
33823
+ optional: false
33824
+ }],
33825
+ "streamCatalog.getCatalog": [{
33826
+ name: "deviceId",
33827
+ form: "single",
33828
+ optional: false
33829
+ }],
33830
+ "streamParams.getConfigSchema": [{
33831
+ name: "deviceId",
33832
+ form: "single",
33833
+ optional: false
33834
+ }],
33835
+ "streamParams.getOptions": [{
33836
+ name: "deviceId",
33837
+ form: "single",
33838
+ optional: false
33839
+ }],
33840
+ "streamParams.setProfile": [{
33841
+ name: "deviceId",
33842
+ form: "single",
33843
+ optional: false
33844
+ }],
33845
+ "switch.setState": [{
33846
+ name: "deviceId",
33847
+ form: "single",
33848
+ optional: false
33849
+ }],
33850
+ "vacuumControl.locate": [{
33851
+ name: "deviceId",
33852
+ form: "single",
33853
+ optional: false
33854
+ }],
33855
+ "vacuumControl.pause": [{
33856
+ name: "deviceId",
33857
+ form: "single",
33858
+ optional: false
33859
+ }],
33860
+ "vacuumControl.returnToBase": [{
33861
+ name: "deviceId",
33862
+ form: "single",
33863
+ optional: false
33864
+ }],
33865
+ "vacuumControl.setFanSpeed": [{
33866
+ name: "deviceId",
33867
+ form: "single",
33868
+ optional: false
33869
+ }],
33870
+ "vacuumControl.start": [{
33871
+ name: "deviceId",
33872
+ form: "single",
33873
+ optional: false
33874
+ }],
33875
+ "vacuumControl.stop": [{
33876
+ name: "deviceId",
33877
+ form: "single",
33878
+ optional: false
33879
+ }],
33880
+ "valve.close": [{
33881
+ name: "deviceId",
33882
+ form: "single",
33883
+ optional: false
33884
+ }],
33885
+ "valve.open": [{
33886
+ name: "deviceId",
33887
+ form: "single",
33888
+ optional: false
33889
+ }],
33890
+ "valve.setPosition": [{
33891
+ name: "deviceId",
33892
+ form: "single",
33893
+ optional: false
33894
+ }],
33895
+ "valve.stop": [{
33896
+ name: "deviceId",
33897
+ form: "single",
33898
+ optional: false
33899
+ }],
33900
+ "videoclips.getClipPlayback": [{
33901
+ name: "deviceId",
33902
+ form: "single",
33903
+ optional: false
33904
+ }],
33905
+ "videoclips.listClips": [{
33906
+ name: "deviceId",
33907
+ form: "single",
33908
+ optional: false
33909
+ }],
33910
+ "waterHeater.setAway": [{
33911
+ name: "deviceId",
33912
+ form: "single",
33913
+ optional: false
33914
+ }],
33915
+ "waterHeater.setOperationMode": [{
33916
+ name: "deviceId",
33917
+ form: "single",
33918
+ optional: false
33919
+ }],
33920
+ "waterHeater.setTargetTemp": [{
33921
+ name: "deviceId",
33922
+ form: "single",
33923
+ optional: false
33924
+ }],
33925
+ "webrtcSession.addIceCandidate": [{
33926
+ name: "deviceId",
33927
+ form: "single",
33928
+ optional: false
33929
+ }],
33930
+ "webrtcSession.closeSession": [{
33931
+ name: "deviceId",
33932
+ form: "single",
33933
+ optional: false
33934
+ }],
33935
+ "webrtcSession.createSession": [{
33936
+ name: "deviceId",
33937
+ form: "single",
33938
+ optional: false
33939
+ }],
33940
+ "webrtcSession.getIceCandidates": [{
33941
+ name: "deviceId",
33942
+ form: "single",
33943
+ optional: false
33944
+ }],
33945
+ "webrtcSession.getSessionState": [{
33946
+ name: "deviceId",
33947
+ form: "single",
33948
+ optional: false
33949
+ }],
33950
+ "webrtcSession.handleAnswer": [{
33951
+ name: "deviceId",
33952
+ form: "single",
33953
+ optional: false
33954
+ }],
33955
+ "webrtcSession.handleOffer": [{
33956
+ name: "deviceId",
33957
+ form: "single",
33958
+ optional: false
33959
+ }],
33960
+ "webrtcSession.hasAdaptiveBitrate": [{
33961
+ name: "deviceId",
33962
+ form: "single",
33963
+ optional: false
33964
+ }],
33965
+ "webrtcSession.listStreams": [{
33966
+ name: "deviceId",
33967
+ form: "single",
33968
+ optional: false
33969
+ }],
33970
+ "zoneAnalytics.getCameraHistory": [{
33971
+ name: "deviceId",
33972
+ form: "single",
33973
+ optional: false
33974
+ }],
33975
+ "zoneAnalytics.getCurrentSnapshot": [{
33976
+ name: "deviceId",
33977
+ form: "single",
33978
+ optional: false
33979
+ }],
33980
+ "zoneAnalytics.getUnzonedHistory": [{
33981
+ name: "deviceId",
33982
+ form: "single",
33983
+ optional: false
33984
+ }],
33985
+ "zoneAnalytics.getZoneHistory": [{
33986
+ name: "deviceId",
33987
+ form: "single",
33988
+ optional: false
33989
+ }],
33990
+ "zoneRules.listRules": [{
33991
+ name: "deviceId",
33992
+ form: "single",
33993
+ optional: false
33994
+ }],
33995
+ "zoneRules.setRules": [{
33996
+ name: "deviceId",
33997
+ form: "single",
33998
+ optional: false
33999
+ }],
34000
+ "zones.addZone": [{
34001
+ name: "deviceId",
34002
+ form: "single",
34003
+ optional: false
34004
+ }],
34005
+ "zones.listZones": [{
34006
+ name: "deviceId",
34007
+ form: "single",
34008
+ optional: false
34009
+ }],
34010
+ "zones.removeZone": [{
34011
+ name: "deviceId",
34012
+ form: "single",
34013
+ optional: false
34014
+ }],
34015
+ "zones.updateZone": [{
34016
+ name: "deviceId",
34017
+ form: "single",
34018
+ optional: false
34019
+ }]
34020
+ });
31378
34021
  Object.freeze({
31379
34022
  "broker": "broker",
31380
34023
  "device-export": "device-export",
@@ -32094,7 +34737,7 @@ var Fmp4FragmentChild = class {
32094
34737
  meta: {
32095
34738
  sourceId: this.args.sourceId,
32096
34739
  decodeHwAccel: requested,
32097
- error: errMsg$12(err)
34740
+ error: errMsg$15(err)
32098
34741
  }
32099
34742
  });
32100
34743
  this.killChild();
@@ -32273,7 +34916,7 @@ var Fmp4FragmentChild = class {
32273
34916
  tags: { deviceId: this.args.deviceId },
32274
34917
  meta: {
32275
34918
  sourceId: this.args.sourceId,
32276
- error: errMsg$12(err)
34919
+ error: errMsg$15(err)
32277
34920
  }
32278
34921
  });
32279
34922
  }
@@ -78618,7 +81261,7 @@ function clearPairingFiles(accessoryUuid, logger) {
78618
81261
  }
78619
81262
  //#endregion
78620
81263
  //#region src/hap-setup-uri.ts
78621
- function errMsg$11(e) {
81264
+ function errMsg$14(e) {
78622
81265
  return e instanceof Error ? e.message : String(e);
78623
81266
  }
78624
81267
  /**
@@ -78645,11 +81288,79 @@ function firstExposedAccessorySetupUri(exposed, logger) {
78645
81288
  try {
78646
81289
  return first.setupURI();
78647
81290
  } catch (err) {
78648
- logger.debug("export-hap: setupURI failed on first exposed accessory", { meta: { error: errMsg$11(err) } });
81291
+ logger.debug("export-hap: setupURI failed on first exposed accessory", { meta: { error: errMsg$14(err) } });
78649
81292
  return;
78650
81293
  }
78651
81294
  }
78652
81295
  }
81296
+ //#endregion
81297
+ //#region src/mappers/builders/accessory-info.ts
81298
+ /**
81299
+ * `Service.AccessoryInformation` — the manufacturer / model / firmware /
81300
+ * serial block every HomeKit accessory carries.
81301
+ *
81302
+ * One implementation for both accessory shapes (camera and generic): the
81303
+ * fields come from the device's own metadata either way, and a second copy
81304
+ * would be a second answer to "what serial does this device publish".
81305
+ * Metadata is best-effort — a device that cannot answer still publishes.
81306
+ */
81307
+ async function populateAccessoryInfo(accessory, proxy, displayName, modelFallback) {
81308
+ const info = accessory.getService(import_dist.Service.AccessoryInformation);
81309
+ if (!info) return;
81310
+ try {
81311
+ const device = await proxy.deviceManager?.getDevice({});
81312
+ const metadata = readMetadata(device);
81313
+ info.setCharacteristic(import_dist.Characteristic.Name, device?.name ?? displayName);
81314
+ info.setCharacteristic(import_dist.Characteristic.Manufacturer, stringOr(metadata?.manufacturer, "CamStack"));
81315
+ info.setCharacteristic(import_dist.Characteristic.Model, stringOr(metadata?.model, modelFallback));
81316
+ info.setCharacteristic(import_dist.Characteristic.FirmwareRevision, stringOr(metadata?.firmware, "0.0.0"));
81317
+ info.setCharacteristic(import_dist.Characteristic.SerialNumber, stringOr(metadata?.sn, `camstack-${proxy.deviceId}`));
81318
+ } catch {}
81319
+ }
81320
+ /** The four fields this module reads, or `null` — the device record's
81321
+ * `metadata` is an open bag and nothing else here depends on its shape. */
81322
+ function readMetadata(device) {
81323
+ const metadata = device?.metadata;
81324
+ if (metadata === null || typeof metadata !== "object") return {};
81325
+ const entries = new Map(Object.entries(metadata));
81326
+ return {
81327
+ model: stringOrNull(entries.get("model")),
81328
+ manufacturer: stringOrNull(entries.get("manufacturer")),
81329
+ firmware: stringOrNull(entries.get("firmware")),
81330
+ sn: stringOrNull(entries.get("sn"))
81331
+ };
81332
+ }
81333
+ function stringOrNull(value) {
81334
+ return typeof value === "string" ? value : null;
81335
+ }
81336
+ function stringOr(value, fallback) {
81337
+ return typeof value === "string" && value.length > 0 ? value : fallback;
81338
+ }
81339
+ //#endregion
81340
+ //#region src/mappers/builders/generic/characteristic-update.ts
81341
+ /**
81342
+ * Parse `status` with the capability's OWN Zod schema and turn it into
81343
+ * characteristic writes.
81344
+ *
81345
+ * Duck-typing the payload here would be the second source of truth about what a
81346
+ * cap reports; the schema is the first and only one. A payload that does not
81347
+ * match yields NO updates — never a partial or invented value — and the caller
81348
+ * logs the drop, because a sensor that silently stops moving is
81349
+ * indistinguishable from a sensor that never changed.
81350
+ *
81351
+ * Rows `.pick()` only the fields they read, so a provider omitting a timestamp
81352
+ * cannot silence a sensor.
81353
+ */
81354
+ function reader(schema, toUpdates) {
81355
+ return (status) => {
81356
+ const parsed = schema.safeParse(status);
81357
+ return parsed.success ? toUpdates(parsed.data) : [];
81358
+ };
81359
+ }
81360
+ /** Push every update onto `service`. */
81361
+ function applyUpdates(service, updates) {
81362
+ for (const update of updates) service.updateCharacteristic(update.characteristic, update.value);
81363
+ }
78653
81364
  /**
78654
81365
  * hap-nodejs' `checkName` regex, verbatim.
78655
81366
  *
@@ -78786,6 +81497,92 @@ function titleCase(raw) {
78786
81497
  return raw.split(/[-_\s]+/u).filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
78787
81498
  }
78788
81499
  //#endregion
81500
+ //#region src/mappers/builders/service-label.ts
81501
+ /**
81502
+ * The ONE place a secondary service on the camera accessory gets its label.
81503
+ *
81504
+ * A "secondary service" here is a Switch or Lightbulb published alongside the
81505
+ * camera on the same accessory — the privacy switch, each accessory child
81506
+ * (siren, floodlight), each PTZ action. iOS Home renders these as their own
81507
+ * controls, and the operator has seen them as "Interruttore 1", "Interruttore
81508
+ * 2" through three separate rounds of fixes.
81509
+ *
81510
+ * ## Why `Name` alone cannot rename anything
81511
+ *
81512
+ * Two facts about hap-nodejs 2.1.7, both measured against the installed copy
81513
+ * rather than reasoned about:
81514
+ *
81515
+ * 1. `accessory.addService(Type, displayName, subtype)` ALREADY writes
81516
+ * `displayName` to `Characteristic.Name` (`Service` constructor). So every
81517
+ * round of this bug — including the one that moved the label onto
81518
+ * `ConfiguredName` — shipped with `Name` correctly set. "iOS had no name
81519
+ * to render" was never true.
81520
+ * 2. The mDNS configuration number (`c#`) is a sha1 over
81521
+ * `internalHAPRepresentation(false)`, which OMITS characteristic VALUES.
81522
+ * Changing the string in `Name` therefore does not bump `c#`, a paired
81523
+ * controller gets no signal to re-read `/accessories`, and the name it
81524
+ * cached at first enumeration stands forever.
81525
+ *
81526
+ * `Name` is also declared `pr` only — paired read, no write, no notify. It is
81527
+ * the seed a controller seeds its database from once; it is not a channel.
81528
+ *
81529
+ * ## Why `ConfiguredName`
81530
+ *
81531
+ * `ConfiguredName` (`000000E3`) is declared `pr | pw | ev` — the only name
81532
+ * characteristic a controller may write and may subscribe to. It is what iOS
81533
+ * 16+ reads for a service the user can rename, and adding it CHANGES the
81534
+ * accessory structure, so `c#` does bump and the controller re-reads.
81535
+ *
81536
+ * It was removed once because hap-nodejs logged
81537
+ *
81538
+ * ```
81539
+ * Characteristic not in required or optional characteristic section for
81540
+ * service Switch. Adding anyway.
81541
+ * ```
81542
+ *
81543
+ * That line is a WARNING, not a rejection: `Service.getCharacteristic` calls
81544
+ * `addCharacteristic` unconditionally and only then emits the warning. The
81545
+ * characteristic was always present and always published. hap-nodejs'
81546
+ * per-service optional lists simply predate `ConfiguredName` being valid on
81547
+ * any service.
81548
+ *
81549
+ * Registering it with {@link Service.addOptionalCharacteristic} first takes
81550
+ * the branch above the warning, so the accessory still builds with ZERO
81551
+ * characteristic warnings — which is what `service-naming.spec.ts` asserts.
81552
+ *
81553
+ * ## Scope: EVERY service, including the sensors
81554
+ *
81555
+ * An earlier round applied this to Switch- and Lightbulb-shaped services only,
81556
+ * on the theory that `Service.MotionSensor` and `Service.Battery` are not
81557
+ * separately named tiles in iOS Home and that naming them would be a guess.
81558
+ *
81559
+ * That theory was never measured, and it had a cost: it left services on the
81560
+ * accessory whose name a paired controller could never be told about, and it
81561
+ * made "did `ConfiguredName` fix the operator's 'Interruttore N'?" unanswerable
81562
+ * — a negative result on a partial application proves nothing about the
81563
+ * mechanism. Every service this addon publishes now carries both
81564
+ * characteristics, on the camera accessory and on the generic one.
81565
+ *
81566
+ * The reasoning above is mechanism, not measurement: it says why `Name` alone
81567
+ * CANNOT work and why `ConfiguredName` is the only characteristic that can. It
81568
+ * does not prove iOS renders it on every service shape. That is an observation
81569
+ * only a re-paired controller can make — and after a change like this one, the
81570
+ * controller must be re-paired, because a cached accessory database is exactly
81571
+ * what the whole mechanism is about.
81572
+ */
81573
+ /**
81574
+ * Publish `name` as both the immutable `Name` and the controller-visible
81575
+ * `ConfiguredName` of `service`.
81576
+ *
81577
+ * `name` must already be HAP-valid — build it with `service-names.ts`, which
81578
+ * cannot return a string hap-nodejs' `checkName` would warn about.
81579
+ */
81580
+ function applyServiceLabel(service, name) {
81581
+ service.setCharacteristic(import_dist.Characteristic.Name, name);
81582
+ if (!service.optionalCharacteristics.some((characteristic) => characteristic.UUID === import_dist.Characteristic.ConfiguredName.UUID)) service.addOptionalCharacteristic(import_dist.Characteristic.ConfiguredName);
81583
+ service.setCharacteristic(import_dist.Characteristic.ConfiguredName, name);
81584
+ }
81585
+ //#endregion
78789
81586
  //#region src/mappers/builders/battery.ts
78790
81587
  /**
78791
81588
  * Battery builder — surfaces a battery-operated camera's power state
@@ -78795,34 +81592,56 @@ function titleCase(raw) {
78795
81592
  * subscribes to the runtime-state slice so iOS Home reflects level /
78796
81593
  * charging changes pushed by the firmware without a poll loop.
78797
81594
  *
81595
+ * The status→characteristic mapping is {@link batteryCharacteristicUpdates},
81596
+ * exported because the generic (non-camera) export path publishes the same
81597
+ * `Service.Battery` from the same cap — one derivation, two accessory shapes.
81598
+ *
81599
+ * Skipped silently when the `battery` cap is not bound — caller checks
81600
+ * cap presence before invoking this builder (see `camera-accessory.ts`).
81601
+ */
81602
+ var LOW_BATTERY_THRESHOLD_PCT = 20;
81603
+ /**
78798
81604
  * Mapping:
78799
81605
  * - `BatteryStatus.percentage` (0..100) → `Characteristic.BatteryLevel`
78800
81606
  * - `BatteryStatus.charging`:
78801
81607
  * `'none'` → `ChargingState.NOT_CHARGING`
78802
81608
  * `'dc' | 'solar'` → `ChargingState.CHARGING`
78803
81609
  * - `percentage <= LOW_BATTERY_THRESHOLD_PCT` → `StatusLowBattery.LOW`
78804
- *
78805
- * Skipped silently when the `battery` cap is not bound — caller checks
78806
- * cap presence before invoking this builder (see `camera-accessory.ts`).
78807
81610
  */
78808
- var LOW_BATTERY_THRESHOLD_PCT = 20;
81611
+ var batteryCharacteristicUpdates = reader(BatteryStatusSchema.pick({ percentage: true }).extend({ charging: BatteryStatusSchema.shape.charging.optional() }), (status) => {
81612
+ const pct = Math.max(0, Math.min(100, Math.round(status.percentage)));
81613
+ return [
81614
+ {
81615
+ characteristic: import_dist.Characteristic.BatteryLevel,
81616
+ value: pct
81617
+ },
81618
+ ...status.charging === void 0 ? [] : [{
81619
+ characteristic: import_dist.Characteristic.ChargingState,
81620
+ value: status.charging === "none" ? import_dist.Characteristic.ChargingState.NOT_CHARGING : import_dist.Characteristic.ChargingState.CHARGING
81621
+ }],
81622
+ {
81623
+ characteristic: import_dist.Characteristic.StatusLowBattery,
81624
+ value: pct <= LOW_BATTERY_THRESHOLD_PCT ? import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
81625
+ }
81626
+ ];
81627
+ });
78809
81628
  async function buildBattery(bctx) {
78810
81629
  const { ctx, accessory, proxy, numericDeviceId, displayName } = bctx;
78811
81630
  const log = ctx.logger.withTags({ deviceId: numericDeviceId });
78812
- const service = accessory.addService(import_dist.Service.Battery, hapServiceName([displayName], `Camera ${numericDeviceId}`));
81631
+ const label = hapServiceName([displayName], `Camera ${numericDeviceId}`);
81632
+ const service = accessory.addService(import_dist.Service.Battery, label);
81633
+ applyServiceLabel(service, label);
78813
81634
  try {
78814
81635
  const status = await proxy.battery?.getStatus({});
78815
- if (status) applyToService(service, status);
81636
+ if (status !== void 0 && status !== null) applyToService(service, status);
78816
81637
  } catch (err) {
78817
- log.debug("export-hap: battery getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$10(err) } });
81638
+ log.debug("export-hap: battery getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$13(err) } });
78818
81639
  }
78819
81640
  const unsubscribes = [];
78820
81641
  if (proxy.state.battery) {
78821
81642
  const unsub = proxy.state.battery.subscribe((value) => {
78822
81643
  if (!value) return;
78823
- const status = value;
78824
- if (typeof status.percentage !== "number") return;
78825
- applyToService(service, status);
81644
+ applyToService(service, value);
78826
81645
  });
78827
81646
  unsubscribes.push(unsub);
78828
81647
  }
@@ -78833,14 +81652,9 @@ async function buildBattery(bctx) {
78833
81652
  } };
78834
81653
  }
78835
81654
  function applyToService(service, status) {
78836
- const pct = Math.max(0, Math.min(100, Math.round(status.percentage)));
78837
- service.updateCharacteristic(import_dist.Characteristic.BatteryLevel, pct);
78838
- const chargingState = status.charging === "none" ? import_dist.Characteristic.ChargingState.NOT_CHARGING : import_dist.Characteristic.ChargingState.CHARGING;
78839
- service.updateCharacteristic(import_dist.Characteristic.ChargingState, chargingState);
78840
- const lowBattery = pct <= LOW_BATTERY_THRESHOLD_PCT ? import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
78841
- service.updateCharacteristic(import_dist.Characteristic.StatusLowBattery, lowBattery);
81655
+ applyUpdates(service, batteryCharacteristicUpdates(status));
78842
81656
  }
78843
- function errMsg$10(err) {
81657
+ function errMsg$13(err) {
78844
81658
  return err instanceof Error ? err.message : String(err);
78845
81659
  }
78846
81660
  //#endregion
@@ -89592,7 +92406,7 @@ var VIDEO_LOOPBACK_RCVBUF_BYTES = 8 * 1024 * 1024;
89592
92406
  * not burst size.
89593
92407
  */
89594
92408
  var AUDIO_LOOPBACK_RCVBUF_BYTES = 1024 * 1024;
89595
- function errMsg$9(err) {
92409
+ function errMsg$12(err) {
89596
92410
  return err instanceof Error ? err.message : String(err);
89597
92411
  }
89598
92412
  /**
@@ -89607,13 +92421,13 @@ function applyReceiveBuffer(socket, requestedBytes) {
89607
92421
  try {
89608
92422
  socket.setRecvBufferSize(requestedBytes);
89609
92423
  } catch (err) {
89610
- error = errMsg$9(err);
92424
+ error = errMsg$12(err);
89611
92425
  }
89612
92426
  let effectiveBytes = null;
89613
92427
  try {
89614
92428
  effectiveBytes = socket.getRecvBufferSize();
89615
92429
  } catch (err) {
89616
- if (error === null) error = errMsg$9(err);
92430
+ if (error === null) error = errMsg$12(err);
89617
92431
  }
89618
92432
  return {
89619
92433
  requestedBytes,
@@ -89873,20 +92687,20 @@ function buildCameraStreamingDelegate(bctx, advertised) {
89873
92687
  delegate: {
89874
92688
  handleSnapshotRequest(request, callback) {
89875
92689
  handleSnapshot(bctx, request).then((buf) => callback(void 0, buf)).catch((err) => {
89876
- log.warn("export-hap: snapshot failed", { meta: { error: errMsg$8(err) } });
89877
- callback(err instanceof Error ? err : new Error(errMsg$8(err)));
92690
+ log.warn("export-hap: snapshot failed", { meta: { error: errMsg$11(err) } });
92691
+ callback(err instanceof Error ? err : new Error(errMsg$11(err)));
89878
92692
  });
89879
92693
  },
89880
92694
  prepareStream(request, callback) {
89881
92695
  prepareStream(request, sessions, bctx).then((resp) => callback(void 0, resp)).catch((err) => {
89882
- log.warn("export-hap: prepareStream failed", { meta: { error: errMsg$8(err) } });
89883
- callback(err instanceof Error ? err : new Error(errMsg$8(err)));
92696
+ log.warn("export-hap: prepareStream failed", { meta: { error: errMsg$11(err) } });
92697
+ callback(err instanceof Error ? err : new Error(errMsg$11(err)));
89884
92698
  });
89885
92699
  },
89886
92700
  handleStreamRequest(request, callback) {
89887
92701
  handleStreamRequest(request, sessions, bctx, advertised).then(() => callback()).catch((err) => {
89888
- log.warn("export-hap: handleStreamRequest failed", { meta: { error: errMsg$8(err) } });
89889
- callback(err instanceof Error ? err : new Error(errMsg$8(err)));
92702
+ log.warn("export-hap: handleStreamRequest failed", { meta: { error: errMsg$11(err) } });
92703
+ callback(err instanceof Error ? err : new Error(errMsg$11(err)));
89890
92704
  });
89891
92705
  }
89892
92706
  },
@@ -90002,7 +92816,7 @@ async function prepareStream(request, sessions, bctx) {
90002
92816
  closeSocket(audioUdp);
90003
92817
  closeSocket(videoLoopUdp);
90004
92818
  closeSocket(audioLoopUdp);
90005
- throw new Error(`export-hap: outbound SrtpSession init failed: ${errMsg$8(err)}`, { cause: err });
92819
+ throw new Error(`export-hap: outbound SrtpSession init failed: ${errMsg$11(err)}`, { cause: err });
90006
92820
  }
90007
92821
  let upstreamAudioSrtp = null;
90008
92822
  try {
@@ -90016,7 +92830,7 @@ async function prepareStream(request, sessions, bctx) {
90016
92830
  profile: import_src.ProtectionProfileAes128CmHmacSha1_80
90017
92831
  });
90018
92832
  } catch (err) {
90019
- bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).warn("export-hap: SrtpSession init failed (upstream audio decrypt disabled)", { meta: { error: errMsg$8(err) } });
92833
+ bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).warn("export-hap: SrtpSession init failed (upstream audio decrypt disabled)", { meta: { error: errMsg$11(err) } });
90020
92834
  }
90021
92835
  const videoSsrc = randomSsrc();
90022
92836
  const audioSsrc = randomSsrc();
@@ -90133,7 +92947,7 @@ async function prepareStream(request, sessions, bctx) {
90133
92947
  });
90134
92948
  audioUdp.on("message", (packet, rinfo) => {
90135
92949
  handleIncomingAudioRtp(session, packet, rinfo.address, bctx).catch((err) => {
90136
- bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).debug("export-hap: incoming-audio handler error (dropped)", { meta: { error: errMsg$8(err) } });
92950
+ bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).debug("export-hap: incoming-audio handler error (dropped)", { meta: { error: errMsg$11(err) } });
90137
92951
  });
90138
92952
  });
90139
92953
  logLoopbackBuffer(tagLog, request.sessionID, "video", videoLoop.buffer);
@@ -90329,7 +93143,7 @@ function readControllerRtcp(session, leg, packet, log) {
90329
93143
  } catch (err) {
90330
93144
  drop(session, "inbound-rtcp-decrypt-failed");
90331
93145
  storeReceiverReports(session, leg, recordUnreadableRtcp(tally));
90332
- logUnreadableRtcp(session, leg, `decrypt: ${errMsg$8(err)}`, log);
93146
+ logUnreadableRtcp(session, leg, `decrypt: ${errMsg$11(err)}`, log);
90333
93147
  return;
90334
93148
  }
90335
93149
  const outcome = ingestDecryptedRtcp(plaintext, tally);
@@ -91095,7 +93909,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91095
93909
  session.upstreamRtpDecryptFailures += 1;
91096
93910
  if (session.upstreamRtpDecryptFailures % 100 === 1) log.debug("export-hap: SRTP decrypt failed (rate-limited)", { meta: {
91097
93911
  failures: session.upstreamRtpDecryptFailures,
91098
- error: errMsg$8(err)
93912
+ error: errMsg$11(err)
91099
93913
  } });
91100
93914
  return;
91101
93915
  }
@@ -91109,7 +93923,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91109
93923
  rtpPayloadType = parsedRtp.header.payloadType;
91110
93924
  } catch (err) {
91111
93925
  drop(session, "upstream-parse-failed");
91112
- log.debug("export-hap: RTP parse failed after decrypt", { meta: { error: errMsg$8(err) } });
93926
+ log.debug("export-hap: RTP parse failed after decrypt", { meta: { error: errMsg$11(err) } });
91113
93927
  return;
91114
93928
  }
91115
93929
  const negotiatedAudioPt = session.lastStartParams?.audioPt;
@@ -91133,7 +93947,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91133
93947
  if (session.intercomTalkSessionId === null) {
91134
93948
  session.intercomTalkSessionId = "";
91135
93949
  const opened = await openIntercomTalkSession(bctx).catch((err) => {
91136
- log.warn("export-hap: intercom.startTalkSession failed", { meta: { error: errMsg$8(err) } });
93950
+ log.warn("export-hap: intercom.startTalkSession failed", { meta: { error: errMsg$11(err) } });
91137
93951
  return null;
91138
93952
  });
91139
93953
  if (opened) {
@@ -91160,7 +93974,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91160
93974
  drop(session, "upstream-push-failed");
91161
93975
  log.debug("export-hap: intercom.pushTalkAudio failed (will re-open on next frame)", { meta: {
91162
93976
  sequenceNumber: session.intercomPcmSequence,
91163
- error: errMsg$8(err)
93977
+ error: errMsg$11(err)
91164
93978
  } });
91165
93979
  session.intercomTalkSessionId = null;
91166
93980
  }
@@ -91192,7 +94006,7 @@ async function closeIntercomTalkSession(session, bctx) {
91192
94006
  } catch (err) {
91193
94007
  log.debug("export-hap: intercom.endTalkSession failed (continuing)", { meta: {
91194
94008
  sessionId,
91195
- error: errMsg$8(err)
94009
+ error: errMsg$11(err)
91196
94010
  } });
91197
94011
  }
91198
94012
  }
@@ -91217,7 +94031,7 @@ function closeSocket(udp) {
91217
94031
  function randomSsrc() {
91218
94032
  return Math.floor(Math.random() * 2147483646) + 1 | 0;
91219
94033
  }
91220
- function errMsg$8(err) {
94034
+ function errMsg$11(err) {
91221
94035
  return err instanceof Error ? err.message : String(err);
91222
94036
  }
91223
94037
  //#endregion
@@ -91301,14 +94115,14 @@ async function buildDoorbell(input) {
91301
94115
  }
91302
94116
  log.info("export-hap: doorbell SINGLE_PRESS pushed to HomeKit", { meta: { ...delivery } });
91303
94117
  } catch (err) {
91304
- log.warn("export-hap: ringDoorbell() failed", { meta: { error: errMsg$7(err) } });
94118
+ log.warn("export-hap: ringDoorbell() failed", { meta: { error: errMsg$10(err) } });
91305
94119
  }
91306
94120
  });
91307
94121
  return { async dispose() {
91308
94122
  unsubscribe();
91309
94123
  } };
91310
94124
  }
91311
- function errMsg$7(err) {
94125
+ function errMsg$10(err) {
91312
94126
  return err instanceof Error ? err.message : String(err);
91313
94127
  }
91314
94128
  //#endregion
@@ -91348,13 +94162,15 @@ var RESET_DEBOUNCE_MS = 5e3;
91348
94162
  */
91349
94163
  async function buildMotionSensor(bctx, existing = null) {
91350
94164
  const { ctx, accessory, proxy, numericDeviceId, displayName } = bctx;
91351
- const motionService = existing ?? accessory.addService(import_dist.Service.MotionSensor, hapServiceName([displayName], `Camera ${numericDeviceId}`));
94165
+ const label = hapServiceName([displayName], `Camera ${numericDeviceId}`);
94166
+ const motionService = existing ?? accessory.addService(import_dist.Service.MotionSensor, label);
94167
+ applyServiceLabel(motionService, label);
91352
94168
  motionService.setCharacteristic(import_dist.Characteristic.MotionDetected, false);
91353
94169
  try {
91354
94170
  const detected = await proxy.motion?.isDetected({});
91355
94171
  if (typeof detected === "boolean") motionService.updateCharacteristic(import_dist.Characteristic.MotionDetected, detected);
91356
94172
  } catch (err) {
91357
- ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: initial motion hydrate failed (non-fatal)", { meta: { error: errMsg$6(err) } });
94173
+ ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: initial motion hydrate failed (non-fatal)", { meta: { error: errMsg$9(err) } });
91358
94174
  }
91359
94175
  let resetTimer = null;
91360
94176
  const armReset = () => {
@@ -91388,82 +94204,10 @@ async function buildMotionSensor(bctx, existing = null) {
91388
94204
  }
91389
94205
  } };
91390
94206
  }
91391
- function errMsg$6(err) {
94207
+ function errMsg$9(err) {
91392
94208
  return err instanceof Error ? err.message : String(err);
91393
94209
  }
91394
94210
  //#endregion
91395
- //#region src/mappers/builders/service-label.ts
91396
- /**
91397
- * The ONE place a secondary service on the camera accessory gets its label.
91398
- *
91399
- * A "secondary service" here is a Switch or Lightbulb published alongside the
91400
- * camera on the same accessory — the privacy switch, each accessory child
91401
- * (siren, floodlight), each PTZ action. iOS Home renders these as their own
91402
- * controls, and the operator has seen them as "Interruttore 1", "Interruttore
91403
- * 2" through three separate rounds of fixes.
91404
- *
91405
- * ## Why `Name` alone cannot rename anything
91406
- *
91407
- * Two facts about hap-nodejs 2.1.7, both measured against the installed copy
91408
- * rather than reasoned about:
91409
- *
91410
- * 1. `accessory.addService(Type, displayName, subtype)` ALREADY writes
91411
- * `displayName` to `Characteristic.Name` (`Service` constructor). So every
91412
- * round of this bug — including the one that moved the label onto
91413
- * `ConfiguredName` — shipped with `Name` correctly set. "iOS had no name
91414
- * to render" was never true.
91415
- * 2. The mDNS configuration number (`c#`) is a sha1 over
91416
- * `internalHAPRepresentation(false)`, which OMITS characteristic VALUES.
91417
- * Changing the string in `Name` therefore does not bump `c#`, a paired
91418
- * controller gets no signal to re-read `/accessories`, and the name it
91419
- * cached at first enumeration stands forever.
91420
- *
91421
- * `Name` is also declared `pr` only — paired read, no write, no notify. It is
91422
- * the seed a controller seeds its database from once; it is not a channel.
91423
- *
91424
- * ## Why `ConfiguredName`
91425
- *
91426
- * `ConfiguredName` (`000000E3`) is declared `pr | pw | ev` — the only name
91427
- * characteristic a controller may write and may subscribe to. It is what iOS
91428
- * 16+ reads for a service the user can rename, and adding it CHANGES the
91429
- * accessory structure, so `c#` does bump and the controller re-reads.
91430
- *
91431
- * It was removed once because hap-nodejs logged
91432
- *
91433
- * ```
91434
- * Characteristic not in required or optional characteristic section for
91435
- * service Switch. Adding anyway.
91436
- * ```
91437
- *
91438
- * That line is a WARNING, not a rejection: `Service.getCharacteristic` calls
91439
- * `addCharacteristic` unconditionally and only then emits the warning. The
91440
- * characteristic was always present and always published. hap-nodejs'
91441
- * per-service optional lists simply predate `ConfiguredName` being valid on
91442
- * any service.
91443
- *
91444
- * Registering it with {@link Service.addOptionalCharacteristic} first takes
91445
- * the branch above the warning, so the accessory still builds with ZERO
91446
- * characteristic warnings — which is what `service-naming.spec.ts` asserts.
91447
- *
91448
- * ## Scope
91449
- *
91450
- * Switch- and Lightbulb-shaped services only. `Service.MotionSensor` on a
91451
- * camera accessory is not a separately named tile in iOS Home, so giving it a
91452
- * writable name would be a guess, and this module does not guess.
91453
- */
91454
- /**
91455
- * Publish `name` as both the immutable `Name` and the controller-visible
91456
- * `ConfiguredName` of `service`.
91457
- *
91458
- * `name` must already be HAP-valid — build it with `service-names.ts`, which
91459
- * cannot return a string hap-nodejs' `checkName` would warn about.
91460
- */
91461
- function applyServiceLabel(service, name) {
91462
- service.setCharacteristic(import_dist.Characteristic.Name, name);
91463
- if (!service.optionalCharacteristics.some((characteristic) => characteristic.UUID === import_dist.Characteristic.ConfiguredName.UUID)) service.addOptionalCharacteristic(import_dist.Characteristic.ConfiguredName);
91464
- service.setCharacteristic(import_dist.Characteristic.ConfiguredName, name);
91465
- }
91466
- //#endregion
91467
94211
  //#region src/mappers/builders/privacy-switch.ts
91468
94212
  /**
91469
94213
  * Privacy-mask switch builder — turns the camstack `privacy-mask` cap's
@@ -91491,7 +94235,7 @@ async function buildPrivacySwitch(bctx) {
91491
94235
  const status = await proxy.privacyMask?.getStatus({});
91492
94236
  if (status && typeof status.enabled === "boolean") service.updateCharacteristic(import_dist.Characteristic.On, status.enabled);
91493
94237
  } catch (err) {
91494
- log.debug("export-hap: privacy-mask getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$5(err) } });
94238
+ log.debug("export-hap: privacy-mask getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$8(err) } });
91495
94239
  }
91496
94240
  service.getCharacteristic(import_dist.Characteristic.On).onSet(async (value) => {
91497
94241
  const enabled = value === true;
@@ -91500,7 +94244,7 @@ async function buildPrivacySwitch(bctx) {
91500
94244
  } catch (err) {
91501
94245
  log.warn("export-hap: privacy-mask setMask failed", { meta: {
91502
94246
  enabled,
91503
- error: errMsg$5(err)
94247
+ error: errMsg$8(err)
91504
94248
  } });
91505
94249
  }
91506
94250
  });
@@ -91518,7 +94262,7 @@ async function buildPrivacySwitch(bctx) {
91518
94262
  } catch {}
91519
94263
  } };
91520
94264
  }
91521
- function errMsg$5(err) {
94265
+ function errMsg$8(err) {
91522
94266
  return err instanceof Error ? err.message : String(err);
91523
94267
  }
91524
94268
  //#endregion
@@ -91609,7 +94353,7 @@ async function buildPtz(bctx) {
91609
94353
  } catch (err) {
91610
94354
  log.warn("export-hap: ptz.goToPreset failed", { meta: {
91611
94355
  presetId: preset.id,
91612
- error: errMsg$4(err)
94356
+ error: errMsg$7(err)
91613
94357
  } });
91614
94358
  }
91615
94359
  armReset(() => service.updateCharacteristic(import_dist.Characteristic.On, false), MOMENTARY_RESET_MS);
@@ -91627,14 +94371,14 @@ async function buildPtz(bctx) {
91627
94371
  try {
91628
94372
  await proxy.ptz?.stop({});
91629
94373
  } catch (err) {
91630
- log.debug("export-hap: ptz.stop failed (non-fatal)", { meta: { error: errMsg$4(err) } });
94374
+ log.debug("export-hap: ptz.stop failed (non-fatal)", { meta: { error: errMsg$7(err) } });
91631
94375
  }
91632
94376
  service.updateCharacteristic(import_dist.Characteristic.On, false);
91633
94377
  }, options.ptzPulseMs);
91634
94378
  } catch (err) {
91635
94379
  log.warn("export-hap: ptz.continuousMove failed", { meta: {
91636
94380
  dir: dir.label,
91637
- error: errMsg$4(err)
94381
+ error: errMsg$7(err)
91638
94382
  } });
91639
94383
  service.updateCharacteristic(import_dist.Characteristic.On, false);
91640
94384
  }
@@ -91657,7 +94401,7 @@ async function readPresets(bctx) {
91657
94401
  name: p.name
91658
94402
  }));
91659
94403
  } catch (err) {
91660
- ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: ptz.getPresets failed (non-fatal)", { meta: { error: errMsg$4(err) } });
94404
+ ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: ptz.getPresets failed (non-fatal)", { meta: { error: errMsg$7(err) } });
91661
94405
  return [];
91662
94406
  }
91663
94407
  }
@@ -91672,7 +94416,7 @@ async function tryBuildAutotrack(bctx) {
91672
94416
  const status = await proxy.ptzAutotrack.getStatus({});
91673
94417
  if (status && typeof status.enabled === "boolean") service.updateCharacteristic(import_dist.Characteristic.On, status.enabled);
91674
94418
  } catch (err) {
91675
- log.debug("export-hap: ptzAutotrack.getStatus failed (non-fatal)", { meta: { error: errMsg$4(err) } });
94419
+ log.debug("export-hap: ptzAutotrack.getStatus failed (non-fatal)", { meta: { error: errMsg$7(err) } });
91676
94420
  }
91677
94421
  service.getCharacteristic(import_dist.Characteristic.On).onSet(async (value) => {
91678
94422
  const enabled = value === true;
@@ -91681,13 +94425,13 @@ async function tryBuildAutotrack(bctx) {
91681
94425
  } catch (err) {
91682
94426
  log.warn("export-hap: ptzAutotrack.setEnabled failed", { meta: {
91683
94427
  enabled,
91684
- error: errMsg$4(err)
94428
+ error: errMsg$7(err)
91685
94429
  } });
91686
94430
  }
91687
94431
  });
91688
94432
  return { async dispose() {} };
91689
94433
  }
91690
- function errMsg$4(err) {
94434
+ function errMsg$7(err) {
91691
94435
  return err instanceof Error ? err.message : String(err);
91692
94436
  }
91693
94437
  //#endregion
@@ -92718,13 +95462,13 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92718
95462
  const switchStatus = await proxy.switch?.getStatus({});
92719
95463
  if (switchStatus && typeof switchStatus.on === "boolean") service.updateCharacteristic(import_dist.Characteristic.On, switchStatus.on);
92720
95464
  } catch (err) {
92721
- log.debug("export-hap: child switch.getStatus failed (non-fatal)", { meta: { error: errMsg$3(err) } });
95465
+ log.debug("export-hap: child switch.getStatus failed (non-fatal)", { meta: { error: errMsg$6(err) } });
92722
95466
  }
92723
95467
  if (useLightbulb) try {
92724
95468
  const status = await proxy.brightness?.getStatus({});
92725
95469
  if (status && typeof status.percentage === "number") service.updateCharacteristic(import_dist.Characteristic.Brightness, status.percentage);
92726
95470
  } catch (err) {
92727
- log.debug("export-hap: child brightness.getStatus failed (non-fatal)", { meta: { error: errMsg$3(err) } });
95471
+ log.debug("export-hap: child brightness.getStatus failed (non-fatal)", { meta: { error: errMsg$6(err) } });
92728
95472
  }
92729
95473
  if (hasSwitch) service.getCharacteristic(import_dist.Characteristic.On).onSet(async (value) => {
92730
95474
  const on = value === true;
@@ -92733,7 +95477,7 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92733
95477
  } catch (err) {
92734
95478
  log.warn("export-hap: child switch.setState failed", { meta: {
92735
95479
  on,
92736
- error: errMsg$3(err)
95480
+ error: errMsg$6(err)
92737
95481
  } });
92738
95482
  }
92739
95483
  });
@@ -92745,7 +95489,7 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92745
95489
  } catch (err) {
92746
95490
  log.warn("export-hap: child brightness.setBrightness failed", { meta: {
92747
95491
  percentage,
92748
- error: errMsg$3(err)
95492
+ error: errMsg$6(err)
92749
95493
  } });
92750
95494
  }
92751
95495
  });
@@ -92768,7 +95512,7 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92768
95512
  } catch {}
92769
95513
  } };
92770
95514
  }
92771
- function errMsg$3(err) {
95515
+ function errMsg$6(err) {
92772
95516
  return err instanceof Error ? err.message : String(err);
92773
95517
  }
92774
95518
  //#endregion
@@ -92795,7 +95539,7 @@ async function buildChildServicesFor(input) {
92795
95539
  for (const h of handles) try {
92796
95540
  await h.dispose();
92797
95541
  } catch (err) {
92798
- ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: child service dispose failed (continuing)", { meta: { error: errMsg$2(err) } });
95542
+ ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: child service dispose failed (continuing)", { meta: { error: errMsg$5(err) } });
92799
95543
  }
92800
95544
  } };
92801
95545
  }
@@ -92811,7 +95555,7 @@ async function listChildren(ctx, parentNumericId) {
92811
95555
  features: Array.isArray(c.features) ? c.features.filter((f) => typeof f === "string") : []
92812
95556
  }));
92813
95557
  } catch (err) {
92814
- ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: deviceManager.getChildren failed (non-fatal)", { meta: { error: errMsg$2(err) } });
95558
+ ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: deviceManager.getChildren failed (non-fatal)", { meta: { error: errMsg$5(err) } });
92815
95559
  return [];
92816
95560
  }
92817
95561
  }
@@ -92827,10 +95571,76 @@ function asDeviceType(raw) {
92827
95571
  for (const value of Object.values(DeviceType)) if (value === lower) return value;
92828
95572
  return DeviceType.Generic;
92829
95573
  }
92830
- function errMsg$2(err) {
95574
+ function errMsg$5(err) {
92831
95575
  return err instanceof Error ? err.message : String(err);
92832
95576
  }
92833
95577
  //#endregion
95578
+ //#region src/mappers/kind.ts
95579
+ /**
95580
+ * Which orchestrator a device gets, which device types the picker offers, and
95581
+ * what a device's HomeKit accessory is called on the wire.
95582
+ *
95583
+ * Separate from `index.ts` (the factory registry) only so the orchestrators can
95584
+ * import the uuid convention without importing the registry that imports them.
95585
+ */
95586
+ var SUPPORTED_MAPPER_KINDS = ["camera", "generic"];
95587
+ /**
95588
+ * The device types the Export picker offers, and the answer to the cap's
95589
+ * `listSupportedDeviceKinds`.
95590
+ *
95591
+ * A UI FILTER, nothing more. Whether a device actually exports anything is
95592
+ * decided by its capabilities (`rowsForCaps` in the capability table) — the
95593
+ * doctrine the HA exporter writes down, and the thing that confined the
95594
+ * previous exporter to cameras when it was ignored.
95595
+ *
95596
+ * Tier A: the types whose HomeKit services exist today and need no
95597
+ * feature-dependent shape. `cover`, `climate`, `fan`, `valve`, `humidifier`,
95598
+ * `water-heater`, `alarm-panel`, `button` and `media-player` are deliberately
95599
+ * absent — each needs a service whose semantics the table cannot yet honour,
95600
+ * and offering the tab before the mapping exists is how an operator gets an
95601
+ * accessory that pairs and does nothing.
95602
+ */
95603
+ var HAP_EXPORTABLE_DEVICE_TYPES = [
95604
+ DeviceType.Camera,
95605
+ DeviceType.Light,
95606
+ DeviceType.Switch,
95607
+ DeviceType.Siren,
95608
+ DeviceType.Sensor,
95609
+ DeviceType.Lock,
95610
+ DeviceType.Presence,
95611
+ DeviceType.Generic
95612
+ ];
95613
+ /**
95614
+ * Resolve the orchestrator for a device from its TYPE.
95615
+ *
95616
+ * `null` means "no Export tab": the picker must not offer a type no
95617
+ * orchestrator can build. An UNKNOWN type (the device-manager read failed) maps
95618
+ * to `camera` — that is what every entry persisted before this function existed
95619
+ * carries, and a transient API failure must never re-shape an exposed camera.
95620
+ */
95621
+ function pickMapperKind(deviceType) {
95622
+ if (deviceType === null || deviceType === void 0 || deviceType.length === 0) return "camera";
95623
+ if (deviceType === DeviceType.Camera) return "camera";
95624
+ return HAP_EXPORTABLE_DEVICE_TYPES.find((type) => type === deviceType) === void 0 ? null : "generic";
95625
+ }
95626
+ /**
95627
+ * The deterministic HAP accessory UUID for a device.
95628
+ *
95629
+ * One function, because three call sites depend on the answer agreeing: the
95630
+ * orchestrator that builds the accessory, `unexposeDevice` (which wipes
95631
+ * hap-nodejs' pairing blobs by uuid, on a device whose mapper is already gone)
95632
+ * and the export sync state that records it.
95633
+ *
95634
+ * The camera namespace is FROZEN — every camera paired to date derives its MAC
95635
+ * from `sha256("camstack:hap:" + uuid)` of this exact string, and changing it
95636
+ * would make every paired camera a stranger. Generic devices get their own
95637
+ * namespace so a switch and a camera that happen to share a device id are not
95638
+ * the same HomeKit accessory.
95639
+ */
95640
+ function accessoryUuidFor(kind, deviceId) {
95641
+ return import_dist.uuid.generate(kind === "camera" ? `camstack:camera:${deviceId}` : `camstack:device:${deviceId}`);
95642
+ }
95643
+ //#endregion
92834
95644
  //#region src/mappers/camera-accessory.ts
92835
95645
  /**
92836
95646
  * Camera accessory orchestrator — given a camstack deviceId, builds one
@@ -92863,9 +95673,9 @@ async function buildCameraAccessory(input) {
92863
95673
  const capNames = new Set(proxy.binding?.entries.map((e) => e.capName) ?? []);
92864
95674
  const isDoorbell = capNames.has("doorbell");
92865
95675
  const category = isDoorbell ? import_dist.Categories.VIDEO_DOORBELL : import_dist.Categories.IP_CAMERA;
92866
- const accessory = new import_dist.Accessory(displayName, import_dist.uuid.generate(`camstack:camera:${numericId}`));
95676
+ const accessory = new import_dist.Accessory(displayName, accessoryUuidFor("camera", numericId));
92867
95677
  accessory.category = category;
92868
- await populateAccessoryInfo(accessory, proxy, displayName);
95678
+ await populateAccessoryInfo(accessory, proxy, displayName, "Camera");
92869
95679
  const bctx = {
92870
95680
  ctx,
92871
95681
  accessory,
@@ -92923,55 +95733,462 @@ async function buildCameraAccessory(input) {
92923
95733
  for (const h of handles) try {
92924
95734
  await h.dispose();
92925
95735
  } catch (err) {
92926
- log.debug("export-hap: builder dispose failed (continuing)", { meta: { error: errMsg$1(err) } });
95736
+ log.debug("export-hap: builder dispose failed (continuing)", { meta: { error: errMsg$4(err) } });
92927
95737
  }
92928
95738
  try {
92929
95739
  await streams.dispose();
92930
95740
  } catch (err) {
92931
- log.debug("export-hap: streams dispose failed", { meta: { error: errMsg$1(err) } });
95741
+ log.debug("export-hap: streams dispose failed", { meta: { error: errMsg$4(err) } });
92932
95742
  }
92933
95743
  }
92934
95744
  };
92935
95745
  }
92936
- async function populateAccessoryInfo(accessory, proxy, displayName) {
92937
- const info = accessory.getService(import_dist.Service.AccessoryInformation);
92938
- if (!info) return;
95746
+ function errMsg$4(err) {
95747
+ return err instanceof Error ? err.message : String(err);
95748
+ }
95749
+ //#endregion
95750
+ //#region src/mappers/builders/generic/lock.ts
95751
+ /**
95752
+ * `lock-control` → `Service.LockMechanism`.
95753
+ *
95754
+ * Not a sensor row: HomeKit models a lock as two characteristics, a CURRENT
95755
+ * state the accessory owns and a TARGET state the controller writes, and the
95756
+ * cap's five states do not map onto either one alone.
95757
+ *
95758
+ * ```
95759
+ * cap state LockCurrentState LockTargetState
95760
+ * locked SECURED SECURED
95761
+ * unlocked UNSECURED UNSECURED
95762
+ * locking UNKNOWN SECURED (in flight — do not claim SECURED)
95763
+ * unlocking UNKNOWN UNSECURED
95764
+ * jammed JAMMED unchanged (the controller's intent stands)
95765
+ * ```
95766
+ *
95767
+ * Reporting SECURED while the bolt is still moving is the failure worth naming:
95768
+ * iOS renders the lock as closed, the operator walks away, and the motor stalls
95769
+ * behind them. `UNKNOWN` is the honest answer for an in-flight transition.
95770
+ *
95771
+ * `open` (the cap's third method, for locks with a latch/buzzer) has no HomeKit
95772
+ * counterpart on this service and is deliberately not wired — a Switch that
95773
+ * silently buzzes a door open would be a second knob nobody asked for.
95774
+ */
95775
+ var SUBTYPE = "lock-control";
95776
+ var readLock = reader(LockControlStatusSchema.pick({ state: true }), (status) => {
95777
+ const current = status.state === "locked" ? import_dist.Characteristic.LockCurrentState.SECURED : status.state === "unlocked" ? import_dist.Characteristic.LockCurrentState.UNSECURED : status.state === "jammed" ? import_dist.Characteristic.LockCurrentState.JAMMED : import_dist.Characteristic.LockCurrentState.UNKNOWN;
95778
+ const target = status.state === "locked" || status.state === "locking" ? import_dist.Characteristic.LockTargetState.SECURED : status.state === "unlocked" || status.state === "unlocking" ? import_dist.Characteristic.LockTargetState.UNSECURED : null;
95779
+ return [{
95780
+ characteristic: import_dist.Characteristic.LockCurrentState,
95781
+ value: current
95782
+ }, ...target === null ? [] : [{
95783
+ characteristic: import_dist.Characteristic.LockTargetState,
95784
+ value: target
95785
+ }]];
95786
+ });
95787
+ async function buildLockMechanism(bctx) {
95788
+ const { ctx, accessory, proxy, numericDeviceId, displayName } = bctx;
95789
+ const log = ctx.logger.withTags({ deviceId: numericDeviceId });
95790
+ const label = hapServiceName([displayName], `Lock ${numericDeviceId}`);
95791
+ const service = accessory.addService(import_dist.Service.LockMechanism, label, SUBTYPE);
95792
+ applyServiceLabel(service, label);
95793
+ const apply = (status, source) => {
95794
+ const updates = readLock(status);
95795
+ if (updates.length === 0) {
95796
+ log.debug("export-hap: lock status did not match the cap schema — no update", { meta: { source } });
95797
+ return;
95798
+ }
95799
+ applyUpdates(service, updates);
95800
+ };
92939
95801
  try {
92940
- const device = await proxy.deviceManager?.getDevice({});
92941
- const metadata = device?.metadata ?? null;
92942
- info.setCharacteristic(import_dist.Characteristic.Name, device?.name ?? displayName);
92943
- info.setCharacteristic(import_dist.Characteristic.Manufacturer, stringOr(metadata?.manufacturer, "CamStack"));
92944
- info.setCharacteristic(import_dist.Characteristic.Model, stringOr(metadata?.model, "Camera"));
92945
- info.setCharacteristic(import_dist.Characteristic.FirmwareRevision, stringOr(metadata?.firmware, "0.0.0"));
92946
- info.setCharacteristic(import_dist.Characteristic.SerialNumber, stringOr(metadata?.sn, `camstack-${proxy.deviceId}`));
92947
- } catch {}
95802
+ const status = await proxy.lockControl?.getStatus({});
95803
+ if (status !== void 0 && status !== null) apply(status, "getStatus");
95804
+ } catch (err) {
95805
+ log.debug("export-hap: lock getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$3(err) } });
95806
+ }
95807
+ service.getCharacteristic(import_dist.Characteristic.LockTargetState).onSet(async (value) => {
95808
+ const secure = value === import_dist.Characteristic.LockTargetState.SECURED;
95809
+ try {
95810
+ if (secure) await proxy.lockControl?.lock({});
95811
+ else await proxy.lockControl?.unlock({});
95812
+ } catch (err) {
95813
+ log.warn("export-hap: lock command failed", { meta: {
95814
+ secure,
95815
+ error: errMsg$3(err)
95816
+ } });
95817
+ }
95818
+ });
95819
+ const unsubscribe = proxy.state.lockControl?.subscribe((value) => {
95820
+ if (value === void 0 || value === null) return;
95821
+ apply(value, "slice");
95822
+ }) ?? null;
95823
+ return { async dispose() {
95824
+ try {
95825
+ unsubscribe?.();
95826
+ } catch {}
95827
+ } };
92948
95828
  }
92949
- function stringOr(value, fallback) {
92950
- return typeof value === "string" && value.length > 0 ? value : fallback;
95829
+ function errMsg$3(err) {
95830
+ return err instanceof Error ? err.message : String(err);
95831
+ }
95832
+ //#endregion
95833
+ //#region src/mappers/builders/generic/sensor-service.ts
95834
+ async function buildSensorService(input) {
95835
+ const { bctx, spec, name, subtype } = input;
95836
+ const { ctx, accessory, proxy, numericDeviceId } = bctx;
95837
+ const log = ctx.logger.withTags({ deviceId: numericDeviceId });
95838
+ const label = hapServiceName([name], spec.label);
95839
+ const service = spec.addService(accessory, label, subtype);
95840
+ applyServiceLabel(service, label);
95841
+ const apply = (status, source) => {
95842
+ const updates = spec.read(status);
95843
+ if (updates.length === 0) {
95844
+ log.debug("export-hap: sensor status did not match the cap schema — no update", { meta: {
95845
+ subtype,
95846
+ source
95847
+ } });
95848
+ return;
95849
+ }
95850
+ applyUpdates(service, updates);
95851
+ };
95852
+ try {
95853
+ const status = await spec.getStatus(proxy);
95854
+ if (status !== void 0 && status !== null) apply(status, "getStatus");
95855
+ } catch (err) {
95856
+ log.debug("export-hap: sensor getStatus hydrate failed (non-fatal)", { meta: {
95857
+ subtype,
95858
+ error: errMsg$2(err)
95859
+ } });
95860
+ }
95861
+ const unsubscribe = spec.subscribe(proxy, (value) => {
95862
+ if (value === void 0 || value === null) return;
95863
+ apply(value, "slice");
95864
+ });
95865
+ return { async dispose() {
95866
+ try {
95867
+ unsubscribe?.();
95868
+ } catch {}
95869
+ } };
95870
+ }
95871
+ function errMsg$2(err) {
95872
+ return err instanceof Error ? err.message : String(err);
95873
+ }
95874
+ //#endregion
95875
+ //#region src/mappers/builders/generic/cap-service-table.ts
95876
+ /**
95877
+ * The capability→HomeKit-service TABLE for non-camera devices.
95878
+ *
95879
+ * This is the whole coverage decision for the generic export path, and it is
95880
+ * deliberately a table rather than a `switch` on `DeviceType`. The doctrine is
95881
+ * the one the Home Assistant exporter already writes down: *coverage is decided
95882
+ * by a device's capabilities, not its type — restricting the picker by type is
95883
+ * what confined the previous exporter to cameras.* A `DeviceType` here is only
95884
+ * ever a UI filter (`HAP_EXPORTABLE_DEVICE_TYPES`) or an icon
95885
+ * (`ACCESSORY_CATEGORY_BY_TYPE`); it never decides whether something exports.
95886
+ *
95887
+ * The shape mirrors `CAP_ENTITY_MAP` in
95888
+ * `packages/addon-provider-homeassistant/src/ha-export/entity-catalog.ts` so
95889
+ * the two can be unified later — one row per capability, keyed by the cap name
95890
+ * exactly as it appears in `proxy.binding.entries[].capName`.
95891
+ *
95892
+ * ## Reading a status
95893
+ *
95894
+ * Every row parses the cap's OWN Zod status schema rather than duck-typing the
95895
+ * payload, and it `.pick()`s only the fields it reads: a provider that omits a
95896
+ * timestamp must not silence a sensor, and a provider that sends the wrong
95897
+ * shape must not write a garbage characteristic. A parse that fails yields no
95898
+ * updates, and the builder that owns the service logs the drop — silence reads
95899
+ * as "the sensor never changed".
95900
+ *
95901
+ * ## Tier
95902
+ *
95903
+ * Tier A only: the caps whose HomeKit service exists today and needs no
95904
+ * feature-dependent shape (`cover` needs `cover-positionable`, `climate-control`
95905
+ * needs the dual-setpoint split, `alarm-panel` must survive a refused `arm`).
95906
+ * Those are a separate task; adding a row here must never mean adding a service
95907
+ * whose semantics this file cannot fully honour.
95908
+ */
95909
+ /** HomeKit's floor for `CurrentAmbientLightLevel`; 0 lux is out of range. */
95910
+ var MIN_LUX = 1e-4;
95911
+ var SENSOR_SPECS = {
95912
+ contact: {
95913
+ label: "Contact",
95914
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.ContactSensor, name, subtype),
95915
+ getStatus: (proxy) => proxy.contact?.getStatus({}),
95916
+ subscribe: (proxy, onValue) => proxy.state.contact?.subscribe(onValue) ?? null,
95917
+ read: reader(ContactStatusSchema.pick({ entryOpen: true }), (status) => [{
95918
+ characteristic: import_dist.Characteristic.ContactSensorState,
95919
+ value: status.entryOpen ? import_dist.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : import_dist.Characteristic.ContactSensorState.CONTACT_DETECTED
95920
+ }])
95921
+ },
95922
+ motion: {
95923
+ label: "Motion",
95924
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.MotionSensor, name, subtype),
95925
+ getStatus: (proxy) => proxy.motion?.getStatus({}),
95926
+ subscribe: (proxy, onValue) => proxy.state.motion?.subscribe(onValue) ?? null,
95927
+ read: reader(MotionStatusSchema.pick({ detected: true }), (status) => [{
95928
+ characteristic: import_dist.Characteristic.MotionDetected,
95929
+ value: status.detected
95930
+ }])
95931
+ },
95932
+ presence: {
95933
+ label: "Presence",
95934
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.OccupancySensor, name, subtype),
95935
+ getStatus: (proxy) => proxy.presence?.getStatus({}),
95936
+ subscribe: (proxy, onValue) => proxy.state.presence?.subscribe(onValue) ?? null,
95937
+ read: reader(PresenceStatusSchema.pick({ state: true }), (status) => [{
95938
+ characteristic: import_dist.Characteristic.OccupancyDetected,
95939
+ value: status.state === "home" ? import_dist.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED : import_dist.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED
95940
+ }])
95941
+ },
95942
+ smoke: {
95943
+ label: "Smoke",
95944
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.SmokeSensor, name, subtype),
95945
+ getStatus: (proxy) => proxy.smoke?.getStatus({}),
95946
+ subscribe: (proxy, onValue) => proxy.state.smoke?.subscribe(onValue) ?? null,
95947
+ read: reader(SmokeStatusSchema.pick({ detected: true }), (status) => [{
95948
+ characteristic: import_dist.Characteristic.SmokeDetected,
95949
+ value: status.detected ? import_dist.Characteristic.SmokeDetected.SMOKE_DETECTED : import_dist.Characteristic.SmokeDetected.SMOKE_NOT_DETECTED
95950
+ }])
95951
+ },
95952
+ "carbon-monoxide": {
95953
+ label: "Carbon monoxide",
95954
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.CarbonMonoxideSensor, name, subtype),
95955
+ getStatus: (proxy) => proxy.carbonMonoxide?.getStatus({}),
95956
+ subscribe: (proxy, onValue) => proxy.state.carbonMonoxide?.subscribe(onValue) ?? null,
95957
+ read: reader(CarbonMonoxideStatusSchema.pick({ detected: true }), (status) => [{
95958
+ characteristic: import_dist.Characteristic.CarbonMonoxideDetected,
95959
+ value: status.detected ? import_dist.Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL : import_dist.Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL
95960
+ }])
95961
+ },
95962
+ flood: {
95963
+ label: "Leak",
95964
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.LeakSensor, name, subtype),
95965
+ getStatus: (proxy) => proxy.flood?.getStatus({}),
95966
+ subscribe: (proxy, onValue) => proxy.state.flood?.subscribe(onValue) ?? null,
95967
+ read: reader(FloodStatusSchema.pick({ flooded: true }), (status) => [{
95968
+ characteristic: import_dist.Characteristic.LeakDetected,
95969
+ value: status.flooded ? import_dist.Characteristic.LeakDetected.LEAK_DETECTED : import_dist.Characteristic.LeakDetected.LEAK_NOT_DETECTED
95970
+ }])
95971
+ },
95972
+ "temperature-sensor": {
95973
+ label: "Temperature",
95974
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.TemperatureSensor, name, subtype),
95975
+ getStatus: (proxy) => proxy.temperatureSensor?.getStatus({}),
95976
+ subscribe: (proxy, onValue) => proxy.state.temperatureSensor?.subscribe(onValue) ?? null,
95977
+ read: reader(TemperatureSensorStatusSchema.pick({ celsius: true }), (status) => [{
95978
+ characteristic: import_dist.Characteristic.CurrentTemperature,
95979
+ value: status.celsius
95980
+ }])
95981
+ },
95982
+ "humidity-sensor": {
95983
+ label: "Humidity",
95984
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.HumiditySensor, name, subtype),
95985
+ getStatus: (proxy) => proxy.humiditySensor?.getStatus({}),
95986
+ subscribe: (proxy, onValue) => proxy.state.humiditySensor?.subscribe(onValue) ?? null,
95987
+ read: reader(HumiditySensorStatusSchema.pick({ percent: true }), (status) => [{
95988
+ characteristic: import_dist.Characteristic.CurrentRelativeHumidity,
95989
+ value: status.percent
95990
+ }])
95991
+ },
95992
+ "ambient-light-sensor": {
95993
+ label: "Light level",
95994
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.LightSensor, name, subtype),
95995
+ getStatus: (proxy) => proxy.ambientLightSensor?.getStatus({}),
95996
+ subscribe: (proxy, onValue) => proxy.state.ambientLightSensor?.subscribe(onValue) ?? null,
95997
+ read: reader(AmbientLightSensorStatusSchema.pick({ lux: true }), (status) => [{
95998
+ characteristic: import_dist.Characteristic.CurrentAmbientLightLevel,
95999
+ value: Math.max(MIN_LUX, status.lux)
96000
+ }])
96001
+ }
96002
+ };
96003
+ /**
96004
+ * The `battery` row reads like a sensor but writes three characteristics from
96005
+ * one status, and the camera path publishes the SAME service from the same cap.
96006
+ * The derivation therefore lives once, in `battery.ts`, and both callers read
96007
+ * it from there.
96008
+ */
96009
+ var BATTERY_SPEC = {
96010
+ label: "Battery",
96011
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.Battery, name, subtype),
96012
+ getStatus: (proxy) => proxy.battery?.getStatus({}),
96013
+ subscribe: (proxy, onValue) => proxy.state.battery?.subscribe(onValue) ?? null,
96014
+ read: batteryCharacteristicUpdates
96015
+ };
96016
+ function sensorRow(capName, spec) {
96017
+ return {
96018
+ caps: [capName],
96019
+ label: spec.label,
96020
+ build: ({ bctx, name }) => buildSensorService({
96021
+ bctx,
96022
+ spec,
96023
+ name,
96024
+ subtype: capName
96025
+ })
96026
+ };
96027
+ }
96028
+ /**
96029
+ * THE table. Order matters only for one thing: the first row that matches a
96030
+ * device decides the accessory's category when the device's own type does not.
96031
+ */
96032
+ var HAP_CAP_SERVICES = [
96033
+ {
96034
+ caps: ["switch", "brightness"],
96035
+ label: "Power",
96036
+ build: ({ bctx, name, deviceType }) => buildChildSwitch({
96037
+ ...bctx,
96038
+ displayName: name
96039
+ }, "switch", deviceType)
96040
+ },
96041
+ {
96042
+ caps: ["lock-control"],
96043
+ label: "Lock",
96044
+ build: ({ bctx, name }) => buildLockMechanism({
96045
+ ...bctx,
96046
+ displayName: name
96047
+ })
96048
+ },
96049
+ {
96050
+ caps: ["battery"],
96051
+ label: BATTERY_SPEC.label,
96052
+ build: ({ bctx, name }) => buildSensorService({
96053
+ bctx,
96054
+ spec: BATTERY_SPEC,
96055
+ name,
96056
+ subtype: "battery"
96057
+ })
96058
+ },
96059
+ ...Object.entries(SENSOR_SPECS).map(([capName, spec]) => sensorRow(capName, spec))
96060
+ ];
96061
+ /**
96062
+ * The rows a device's bound capabilities select, in table order.
96063
+ *
96064
+ * A row matches when ANY of its caps is bound: a lamp with `switch` but no
96065
+ * `brightness` is still a Lightbulb-shaped row, and the builder degrades on its
96066
+ * own.
96067
+ */
96068
+ function rowsForCaps(capNames) {
96069
+ return HAP_CAP_SERVICES.filter((row) => row.caps.some((cap) => capNames.has(cap)));
96070
+ }
96071
+ /**
96072
+ * The HomeKit accessory category per camstack device type — the icon iOS Home
96073
+ * draws and nothing else. Absent = `OTHER`, which is a valid accessory that
96074
+ * simply gets the generic tile.
96075
+ *
96076
+ * This is NOT a coverage decision. `HAP_EXPORTABLE_DEVICE_TYPES` (in
96077
+ * `mappers/index.ts`) filters the picker; whether a device exports anything is
96078
+ * decided by `rowsForCaps`.
96079
+ */
96080
+ var ACCESSORY_CATEGORY_BY_TYPE = {
96081
+ [DeviceType.Switch]: import_dist.Categories.SWITCH,
96082
+ [DeviceType.Light]: import_dist.Categories.LIGHTBULB,
96083
+ [DeviceType.Siren]: import_dist.Categories.SWITCH,
96084
+ [DeviceType.Lock]: import_dist.Categories.DOOR_LOCK,
96085
+ [DeviceType.Sensor]: import_dist.Categories.SENSOR,
96086
+ [DeviceType.Presence]: import_dist.Categories.SENSOR
96087
+ };
96088
+ function accessoryCategoryFor(deviceType) {
96089
+ return ACCESSORY_CATEGORY_BY_TYPE[deviceType] ?? import_dist.Categories.OTHER;
96090
+ }
96091
+ //#endregion
96092
+ //#region src/mappers/generic-accessory.ts
96093
+ /**
96094
+ * Generic accessory orchestrator — every camstack device that is NOT a camera.
96095
+ *
96096
+ * Same three steps as the camera orchestrator, and deliberately nothing more:
96097
+ * 1. resolve the typed `DeviceProxy`,
96098
+ * 2. select rows from the capability→service table with the device's BOUND
96099
+ * capabilities (never its type — see `builders/generic/cap-service-table.ts`),
96100
+ * 3. let each row add its service to one `Accessory`.
96101
+ *
96102
+ * It publishes standalone, like the camera path: one accessory, one mDNS
96103
+ * advertisement, one pairing, the shared setup code. This addon is not a
96104
+ * bridge — that decision predates this file and is not re-litigated here.
96105
+ *
96106
+ * ## It REFUSES rather than publishing an empty tile
96107
+ *
96108
+ * A device whose capabilities select no row throws. The alternative is an
96109
+ * accessory carrying nothing but `AccessoryInformation`: it pairs, it appears
96110
+ * in the Home app, it does nothing, and the operator has no way to tell that
96111
+ * from a broken integration. The Export tab is gated on the same question
96112
+ * (`hap-export.addon.ts`), so reaching this throw means the device changed
96113
+ * shape between the tab rendering and the toggle landing.
96114
+ */
96115
+ async function buildGenericAccessory(input) {
96116
+ const { ctx, deviceId, displayName, options } = input;
96117
+ const numericId = Number.parseInt(deviceId, 10);
96118
+ if (!Number.isFinite(numericId)) throw new Error(`export-hap: cannot map device '${deviceId}' — id is not numeric`);
96119
+ const log = ctx.logger.withTags({ deviceId: numericId });
96120
+ const proxy = await ctx.fetchDevice(numericId);
96121
+ const capNames = new Set(proxy.binding?.entries.map((e) => e.capName) ?? []);
96122
+ const rows = rowsForCaps(capNames);
96123
+ if (rows.length === 0) throw new Error(`export-hap: device ${numericId} carries no capability HomeKit can export (bound: ${[...capNames].toSorted().join(", ") || "none"})`);
96124
+ const deviceType = await resolveDeviceType(proxy);
96125
+ const accessory = new import_dist.Accessory(displayName, accessoryUuidFor("generic", numericId));
96126
+ accessory.category = accessoryCategoryFor(deviceType);
96127
+ await populateAccessoryInfo(accessory, proxy, displayName, "Accessory");
96128
+ const bctx = {
96129
+ ctx,
96130
+ accessory,
96131
+ proxy,
96132
+ numericDeviceId: numericId,
96133
+ displayName,
96134
+ options
96135
+ };
96136
+ const handles = [];
96137
+ for (const row of rows) {
96138
+ const name = rows.length === 1 ? displayName : row.label;
96139
+ handles.push(await row.build({
96140
+ bctx,
96141
+ name,
96142
+ deviceType
96143
+ }));
96144
+ }
96145
+ log.info("export-hap: built generic accessory", { meta: {
96146
+ deviceType,
96147
+ services: rows.map((row) => row.caps[0]).join(",")
96148
+ } });
96149
+ return {
96150
+ accessory,
96151
+ accessories: [accessory],
96152
+ async dispose() {
96153
+ for (const handle of handles) try {
96154
+ await handle.dispose();
96155
+ } catch (err) {
96156
+ log.debug("export-hap: generic builder dispose failed (continuing)", { meta: { error: errMsg$1(err) } });
96157
+ }
96158
+ }
96159
+ };
96160
+ }
96161
+ /**
96162
+ * The device's own type, or `Generic`.
96163
+ *
96164
+ * Used ONLY where one capability has two HomeKit shapes — a siren's
96165
+ * `brightness` is alarm volume, not luminosity — and for the accessory's icon.
96166
+ * `Generic` is the safe reading: it is the shape `child-switch.ts` already
96167
+ * treats as "a lamp if it dims, a switch otherwise".
96168
+ */
96169
+ async function resolveDeviceType(proxy) {
96170
+ try {
96171
+ const device = await proxy.deviceManager?.getDevice({});
96172
+ const raw = typeof device?.type === "string" ? device.type.toLowerCase() : "";
96173
+ return Object.values(DeviceType).find((value) => value === raw) ?? DeviceType.Generic;
96174
+ } catch {
96175
+ return DeviceType.Generic;
96176
+ }
92951
96177
  }
92952
96178
  function errMsg$1(err) {
92953
96179
  return err instanceof Error ? err.message : String(err);
92954
96180
  }
92955
96181
  //#endregion
92956
96182
  //#region src/mappers/index.ts
92957
- var SUPPORTED_MAPPER_KINDS = ["camera"];
92958
- var REGISTRY = { camera: buildCameraAccessory };
96183
+ var REGISTRY = {
96184
+ camera: buildCameraAccessory,
96185
+ generic: buildGenericAccessory
96186
+ };
92959
96187
  function getMapperFactory(kind) {
92960
96188
  const factory = REGISTRY[kind];
92961
96189
  if (!factory) throw new Error(`export-hap: no mapper registered for kind '${kind}'`);
92962
96190
  return factory;
92963
96191
  }
92964
- /**
92965
- * Resolve the best-fit mapper kind. With Round 2, the operator just
92966
- * picks a camera and the orchestrator does the rest — the single
92967
- * `camera` kind is returned unconditionally. We keep the function
92968
- * signature for backwards-compat with the addon's existing
92969
- * `exposeDevice` flow and to leave room for future device types (NVR,
92970
- * climate sensor, ...).
92971
- */
92972
- function pickMapperKind(_capabilities) {
92973
- return "camera";
92974
- }
92975
96192
  //#endregion
92976
96193
  //#region src/mappers/builders/stream-hwaccel-memo.ts
92977
96194
  /**
@@ -93109,8 +96326,8 @@ function syncStateToJson(map) {
93109
96326
  //#endregion
93110
96327
  //#region src/hap-export.addon.ts
93111
96328
  /**
93112
- * HomeKit (HAP) export addon — publishes a single HAP bridge process
93113
- * that exposes selected camstack devices as HomeKit accessories.
96329
+ * HomeKit (HAP) export addon — publishes selected camstack devices as
96330
+ * standalone HomeKit accessories.
93114
96331
  *
93115
96332
  * Operator flow:
93116
96333
  * 1. Install + enable the addon (hub-only, group `export-hap`).
@@ -93121,10 +96338,10 @@ function syncStateToJson(map) {
93121
96338
  * 3. The setup URI (`X-HM://…`) is logged AND surfaced via the
93122
96339
  * `getStatus` cap method so the wizard UI can render the QR.
93123
96340
  * 4. Operator opens iOS Home → + → scan QR → enter pincode.
93124
- * 5. Operator hits "Expose to HomeKit" on individual camstack
93125
- * devices; the addon attaches a MotionSensor accessory (MVP)
93126
- * to the bridge and persists the choice via the same
93127
- * `updateGlobalSettings` path.
96341
+ * 5. Operator hits "Expose to HomeKit" on individual camstack devices.
96342
+ * Each exposed device is published as its OWN accessory with its own
96343
+ * mDNS advertisement, sharing one setup code cameras cannot be
96344
+ * bridged, so nothing is.
93128
96345
  *
93129
96346
  * Exception — the hap-nodejs library's own `HAPStorage` lives under
93130
96347
  * `ctx.dataDir/hap-store/`. That directory is library-internal: the
@@ -93133,23 +96350,20 @@ function syncStateToJson(map) {
93133
96350
  * I/O for addon-owned data") explicitly scopes itself to OUR own data;
93134
96351
  * library-managed blobs are out of scope.
93135
96352
  *
93136
- * Round 2 scope: full camera bridge. `exposeDevice({deviceId})` builds
93137
- * one HomeKit Accessory per camstack camera with auto-detected feature
93138
- * services driven by the device's capability binding:
93139
- * - `camera-streams` cap CameraRTPStreamManagement via ffmpeg
93140
- * - `intercom` cap → Microphone + Speaker (audio bridge wiring is a
93141
- * Round 3 follow-up services are declared so iOS Home shows the
93142
- * talk-back button, but PCM upload is not yet routed)
93143
- * - `doorbell` cap DoorbellController + Doorbell service
93144
- * - `motion-detection` cap MotionSensor service
93145
- * - `ptz` cap preset switches + 4 directional momentary switches
93146
- * - `ptz-autotrack` cap → stateful "Autotrack" switch
93147
- * Children (siren, floodlight, spotlight, …) become independent
93148
- * accessories under the same Bridge see `mappers/child-accessory.ts`.
93149
- *
93150
- * What's deferred to Round 3+: cam→browser audio Opus transcode,
93151
- * intercom upload bridge, HomeKit Secure Video, recording, native
93152
- * H.264 stream tap (currently uses RTSP + ffmpeg copy).
96353
+ * ## Two accessory shapes, one rule about coverage
96354
+ *
96355
+ * `exposeDevice({deviceId})` resolves the device's TYPE to a mapper kind
96356
+ * (`mappers/kind.ts`) and hands off:
96357
+ *
96358
+ * - a camera `mappers/camera-accessory.ts`: streams, HKSV recording,
96359
+ * doorbell, motion, intercom, PTZ, battery, privacy and every accessory
96360
+ * child, all as services on one accessory;
96361
+ * - anything else → `mappers/generic-accessory.ts`, driven by the
96362
+ * capabilityservice table in `mappers/builders/generic/`.
96363
+ *
96364
+ * The type only picks the SHAPE. What a device publishes is decided by the
96365
+ * capabilities bound to itrestricting coverage by type is precisely what
96366
+ * confined this exporter to cameras for its first three rounds.
93153
96367
  */
93154
96368
  var DEFAULT_DEVICE_SETTINGS = {
93155
96369
  streamPreference: "auto",
@@ -93192,7 +96406,6 @@ var DEFAULT_CONFIG = {
93192
96406
  fixedPin: "",
93193
96407
  interfaceName: "",
93194
96408
  ptzPulseMs: 400,
93195
- hksvPreview: false,
93196
96409
  identity: {
93197
96410
  username: "",
93198
96411
  pincode: "",
@@ -93308,7 +96521,7 @@ var ExportHapAddon = class extends BaseAddon {
93308
96521
  ...setup ? { setup } : {}
93309
96522
  };
93310
96523
  },
93311
- listSupportedDeviceKinds: async () => [...SUPPORTED_MAPPER_KINDS],
96524
+ listSupportedDeviceKinds: async () => [...HAP_EXPORTABLE_DEVICE_TYPES],
93312
96525
  listExposedDevices: async () => Array.from(this.exposed.entries()).map(([deviceId, m]) => {
93313
96526
  const entry = this.config.exposed.find((e) => e.deviceId === deviceId);
93314
96527
  return {
@@ -93393,9 +96606,10 @@ var ExportHapAddon = class extends BaseAddon {
93393
96606
  log.debug("export-hap: device already exposed — refreshing capabilities");
93394
96607
  await this.detachMapper(deviceId);
93395
96608
  }
93396
- const mapperKind = pickMapperKind(capabilities);
93397
- if (!mapperKind) throw new Error(`export-hap: no mapper for capabilities ${JSON.stringify(capabilities ?? [])}`);
93398
- const displayName = await this.resolveDisplayName(deviceId);
96609
+ const summary = await this.fetchDeviceSummary(numericId);
96610
+ const mapperKind = pickMapperKind(summary?.type);
96611
+ if (!mapperKind) throw new Error(`export-hap: device ${numericId} has type '${summary?.type ?? "unknown"}', which HomeKit export does not support`);
96612
+ const displayName = summary?.name ?? `Device ${deviceId}`;
93399
96613
  const previous = this.config.exposed.find((e) => e.deviceId === deviceId);
93400
96614
  const baseEntry = carryForward({
93401
96615
  deviceId,
@@ -93425,10 +96639,11 @@ var ExportHapAddon = class extends BaseAddon {
93425
96639
  async unexposeDevice(deviceId, options = {}) {
93426
96640
  const numericId = Number.parseInt(deviceId, 10);
93427
96641
  const log = this.ctx.logger.withTags({ deviceId: numericId });
96642
+ const mapperKind = this.findEntry(numericId)?.mapperKind ?? "camera";
93428
96643
  await this.detachMapper(deviceId);
93429
96644
  const next = this.config.exposed.filter((e) => e.deviceId !== deviceId);
93430
96645
  if (next.length !== this.config.exposed.length) await this.updateGlobalSettings({ exposed: next });
93431
- if (options.clearPairing !== false) clearPairingFiles(import_dist.uuid.generate(`camstack:camera:${numericId}`), this.ctx.logger);
96646
+ if (options.clearPairing !== false) clearPairingFiles(accessoryUuidFor(mapperKind, numericId), this.ctx.logger);
93432
96647
  await this.forgetFingerprint(numericId);
93433
96648
  log.info("export-hap: unexposed device");
93434
96649
  }
@@ -93478,10 +96693,10 @@ var ExportHapAddon = class extends BaseAddon {
93478
96693
  /** Pending rebuild timers per deviceId — used to debounce. Cleared
93479
96694
  * on detach so stale timers can't republish a removed accessory. */
93480
96695
  pendingRebuildTimers = /* @__PURE__ */ new Map();
93481
- /** Deterministic HAP accessory UUID for a device (matches `buildCameraAccessory`
93482
- * and `unexposeDevice`). */
96696
+ /** Deterministic HAP accessory UUID for a device the same one the
96697
+ * orchestrator built it with (`mappers/kind.ts`). */
93483
96698
  hapAccessoryUuid(deviceId) {
93484
- return import_dist.uuid.generate(`camstack:camera:${deviceId}`);
96699
+ return accessoryUuidFor(this.findEntry(deviceId)?.mapperKind ?? "camera", deviceId);
93485
96700
  }
93486
96701
  /**
93487
96702
  * Export fingerprint of a device from its PERSISTED features + type
@@ -93640,31 +96855,79 @@ var ExportHapAddon = class extends BaseAddon {
93640
96855
  log.warn("export-hap: reconcile rebuild failed", { meta: { error: errMsg(err) } });
93641
96856
  }
93642
96857
  }
93643
- async resolveDisplayName(deviceId) {
93644
- const numeric = Number.parseInt(deviceId, 10);
93645
- if (!Number.isFinite(numeric)) return `Device ${deviceId}`;
96858
+ /**
96859
+ * The device's name and type, or `null` when the registry cannot answer.
96860
+ *
96861
+ * `null` is NOT "no such device" — it also covers a transient API failure,
96862
+ * and every caller treats it as "keep doing what was already being done"
96863
+ * rather than re-shaping an exposed accessory on incomplete information.
96864
+ */
96865
+ async fetchDeviceSummary(deviceId) {
96866
+ if (!Number.isFinite(deviceId)) return null;
96867
+ try {
96868
+ const device = await this.ctx.api.deviceManager?.getDevice.query({ deviceId });
96869
+ if (!device) return null;
96870
+ return {
96871
+ name: device.name,
96872
+ type: device.type
96873
+ };
96874
+ } catch (err) {
96875
+ this.ctx.logger.withTags({ deviceId }).debug("export-hap: deviceManager.getDevice failed", { meta: { error: errMsg(err) } });
96876
+ return null;
96877
+ }
96878
+ }
96879
+ /**
96880
+ * Which accessory shape this device would get, or `null` for "no Export tab".
96881
+ *
96882
+ * Two questions, in this order, because they fail differently:
96883
+ * 1. the TYPE — a type with no orchestrator is refused outright;
96884
+ * 2. for a non-camera, the CAPABILITIES — a device carrying nothing the
96885
+ * table maps would publish an accessory that pairs and does nothing, and
96886
+ * the operator cannot tell that from a broken integration.
96887
+ *
96888
+ * A camera skips (2): the camera orchestrator has always published on type
96889
+ * alone, and adding a cap gate here would be a new way for an existing camera
96890
+ * to lose its Export tab. A registry that cannot answer at all also resolves
96891
+ * to `camera`, which is what every persisted entry predating this method
96892
+ * carries.
96893
+ */
96894
+ async resolveExportKind(deviceId) {
96895
+ const summary = await this.fetchDeviceSummary(deviceId);
96896
+ const kind = pickMapperKind(summary?.type);
96897
+ if (kind === null) return null;
96898
+ if (kind === "camera") return "camera";
93646
96899
  try {
93647
- const device = await this.ctx.api.deviceManager?.getDevice.query({ deviceId: numeric });
93648
- if (device?.name) return device.name;
96900
+ const proxy = await this.ctx.fetchDevice(deviceId);
96901
+ const capNames = new Set(proxy.binding?.entries.map((entry) => entry.capName) ?? []);
96902
+ if (rowsForCaps(capNames).length > 0) return "generic";
96903
+ this.ctx.logger.withTags({ deviceId }).debug("export-hap: no Export tab — no mapped caps", { meta: {
96904
+ type: summary?.type,
96905
+ caps: [...capNames].toSorted().join(",")
96906
+ } });
96907
+ return null;
93649
96908
  } catch (err) {
93650
- this.ctx.logger.withTags({ deviceId: numeric }).debug("export-hap: deviceManager.getDevice failed", { meta: { error: errMsg(err) } });
96909
+ this.ctx.logger.withTags({ deviceId }).debug("export-hap: no Export tab — capability read failed", { meta: {
96910
+ type: summary?.type,
96911
+ error: errMsg(err)
96912
+ } });
96913
+ return null;
93651
96914
  }
93652
- return `Device ${deviceId}`;
93653
96915
  }
93654
96916
  globalSettingsSchema() {
93655
96917
  return this.schema({ sections: [{
93656
96918
  id: "export-hap",
93657
96919
  title: "HomeKit Export",
93658
- description: "Publishes a HomeKit bridge. After the addon boots, scan the pairing QR (or enter the setup code) from the Exported devices panel in the iOS Home app. After pairing, mark individual devices as \"Expose to HomeKit\" on each device's settings page.",
96920
+ description: "Publishes each exposed device as its own HomeKit accessory, sharing one setup code. Mark a device \"Expose to HomeKit\" on its settings page, then add it in the iOS Home app (+ Add Accessory) and enter the setup code shown there.",
93659
96921
  columns: 1,
93660
96922
  fields: [
93661
96923
  this.field({
93662
96924
  type: "text",
93663
96925
  key: "bridgeName",
93664
- label: "Bridge name",
93665
- description: "Name shown in iOS Home and in the Bonjour advertisement.",
96926
+ label: "Installation name",
96927
+ description: "Legacy. Every exposed device is published STANDALONE, under its own camstack device name — HomeKit cameras cannot be bridged, so nothing is bridged. This string only seeds the identity generated on first boot and is not shown in iOS Home.",
93666
96928
  default: DEFAULT_CONFIG.bridgeName,
93667
- requiresRestart: true
96929
+ requiresRestart: true,
96930
+ placement: { tab: "advanced" }
93668
96931
  }),
93669
96932
  this.field({
93670
96933
  type: "number",
@@ -93700,21 +96963,13 @@ var ExportHapAddon = class extends BaseAddon {
93700
96963
  description: "Duration of a single pan/tilt momentary command issued by the PTZ direction switches in Apple Home. Lower = finer steps; higher = bigger sweeps per tap. Defaults to 400ms.",
93701
96964
  default: DEFAULT_CONFIG.ptzPulseMs,
93702
96965
  placement: { tab: "advanced" }
93703
- }),
93704
- this.field({
93705
- type: "boolean",
93706
- key: "hksvPreview",
93707
- label: "HKSV Developer Preview (experimental)",
93708
- description: "Advertise Apple's HomeKit Secure Video Developer Preview services — HEVC streaming and the WebRTC stream-management surface — alongside the classic camera profile. Apple published this specification on 2026-06-03 and no shipping controller is known to negotiate it yet. Leave off unless you are testing against a preview build: advertising unknown services to a paired controller can disturb the classic path that works today.",
93709
- default: DEFAULT_CONFIG.hksvPreview,
93710
- requiresRestart: true,
93711
- placement: { tab: "advanced" }
93712
96966
  })
93713
96967
  ]
93714
96968
  }] });
93715
96969
  }
93716
96970
  async buildDeviceSettingsContribution(deviceId) {
93717
- if (!await this.isCameraDevice(deviceId)) return null;
96971
+ const kind = await this.resolveExportKind(deviceId);
96972
+ if (kind === null) return null;
93718
96973
  const entry = this.findEntry(deviceId);
93719
96974
  const settings = entry?.settings ?? DEFAULT_DEVICE_SETTINGS;
93720
96975
  const enabled = entry !== null;
@@ -93731,6 +96986,32 @@ var ExportHapAddon = class extends BaseAddon {
93731
96986
  } catch (err) {
93732
96987
  this.ctx.logger.withTags({ deviceId }).debug("export-hap: setupURI failed for per-device contribution", { meta: { error: errMsg(err) } });
93733
96988
  }
96989
+ const cameraFields = kind !== "camera" ? [] : [{
96990
+ type: "select",
96991
+ key: streamPreferenceKey,
96992
+ label: "Source stream (HomeKit)",
96993
+ description: "Which camstack profile slot HomeKit pulls. Auto = the broker picks the slot closest to 1080p at session start.",
96994
+ options: HAP_STREAM_PREFERENCE_OPTIONS,
96995
+ required: true,
96996
+ value: settings.streamPreference,
96997
+ showWhen: {
96998
+ field: enabledKey,
96999
+ equals: true
97000
+ },
97001
+ immediate: true
97002
+ }, {
97003
+ type: "boolean",
97004
+ key: hksvKey,
97005
+ label: "HomeKit recording (Secure Video)",
97006
+ description: "Offer “Stream and Allow Recording” in iOS Home. Requires iCloud+ and a home hub. Keeps a continuous 8s prebuffer for this camera (~0.7% of one CPU core, H.264 sources only).",
97007
+ style: "switch",
97008
+ value: resolveHksvRecording(settings),
97009
+ showWhen: {
97010
+ field: enabledKey,
97011
+ equals: true
97012
+ },
97013
+ immediate: true
97014
+ }];
93734
97015
  return {
93735
97016
  tabs: [{
93736
97017
  id: "export",
@@ -93741,7 +97022,7 @@ var ExportHapAddon = class extends BaseAddon {
93741
97022
  sections: [{
93742
97023
  id: "export-hap",
93743
97024
  title: "HomeKit Export",
93744
- description: "Mirror this camera into the HomeKit bridge so iOS Home can pair with it.",
97025
+ description: kind === "camera" ? "Publish this camera as its own HomeKit accessory so iOS Home can pair with it." : "Publish this device as its own HomeKit accessory. Its capabilities decide which controls iOS Home shows.",
93745
97026
  tab: "export",
93746
97027
  columns: 1,
93747
97028
  order: 10,
@@ -93757,7 +97038,7 @@ var ExportHapAddon = class extends BaseAddon {
93757
97038
  type: "qr-code",
93758
97039
  key: "__hap-pair-qr",
93759
97040
  label: paired ? "Pairing QR (already paired)" : "Pairing QR",
93760
- caption: paired ? `Already paired with at least one device. Scan from another iPhone / iPad to add it there too. Setup code: ${this.config.identity.pincode}.` : `Scan with the iOS Camera app to pair this camera in HomeKit. Setup code: ${this.config.identity.pincode}.`,
97041
+ caption: paired ? `Already paired with at least one device. Scan from another iPhone / iPad to add it there too. Setup code: ${this.config.identity.pincode}.` : `Scan with the iOS Camera app to pair this accessory in HomeKit. Setup code: ${this.config.identity.pincode}.`,
93761
97042
  value: qrValue,
93762
97043
  size: 192,
93763
97044
  alt: `HomeKit pairing QR for ${name}`,
@@ -93770,38 +97051,12 @@ var ExportHapAddon = class extends BaseAddon {
93770
97051
  type: "boolean",
93771
97052
  key: enabledKey,
93772
97053
  label: "Expose to HomeKit",
93773
- description: "Toggle to publish this camera onto the HAP bridge.",
97054
+ description: "Toggle to publish this device as a HomeKit accessory.",
93774
97055
  style: "switch",
93775
97056
  value: enabled,
93776
97057
  immediate: true
93777
97058
  },
93778
- {
93779
- type: "select",
93780
- key: streamPreferenceKey,
93781
- label: "Source stream (HomeKit)",
93782
- description: "Which camstack profile slot HomeKit pulls. Auto = the broker picks the slot closest to 1080p at session start.",
93783
- options: HAP_STREAM_PREFERENCE_OPTIONS,
93784
- required: true,
93785
- value: settings.streamPreference,
93786
- showWhen: {
93787
- field: enabledKey,
93788
- equals: true
93789
- },
93790
- immediate: true
93791
- },
93792
- {
93793
- type: "boolean",
93794
- key: hksvKey,
93795
- label: "HomeKit recording (Secure Video)",
93796
- description: "Offer “Stream and Allow Recording” in iOS Home. Requires iCloud+ and a home hub. Keeps a continuous 8s prebuffer for this camera (~0.7% of one CPU core, H.264 sources only).",
93797
- style: "switch",
93798
- value: resolveHksvRecording(settings),
93799
- showWhen: {
93800
- field: enabledKey,
93801
- equals: true
93802
- },
93803
- immediate: true
93804
- }
97059
+ ...cameraFields
93805
97060
  ]
93806
97061
  }]
93807
97062
  };
@@ -93867,23 +97122,6 @@ var ExportHapAddon = class extends BaseAddon {
93867
97122
  }
93868
97123
  return { success: true };
93869
97124
  }
93870
- /**
93871
- * Camera-only gate — HomeKit export only knows how to mirror cameras
93872
- * today. Mirrors the snapshot addon's source-side filter so the
93873
- * device-details page doesn't render an "Export" tab on lights /
93874
- * switches / sensors.
93875
- */
93876
- async isCameraDevice(deviceId) {
93877
- const api = this.ctx.api;
93878
- if (!api.deviceManager) return true;
93879
- try {
93880
- const dev = await api.deviceManager.getDevice.query({ deviceId });
93881
- if (!dev) return true;
93882
- return dev.type === DeviceType.Camera;
93883
- } catch {
93884
- return true;
93885
- }
93886
- }
93887
97125
  findEntry(deviceId) {
93888
97126
  const id = String(deviceId);
93889
97127
  return this.config.exposed.find((e) => e.deviceId === id) ?? null;