@camstack/addon-provider-homematic 1.2.6 → 1.2.7

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.
Files changed (3) hide show
  1. package/dist/addon.js +451 -76
  2. package/dist/addon.mjs +451 -76
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -8,7 +8,7 @@ let http = require("http");
8
8
  let crypto$1 = require("crypto");
9
9
  let fs_promises = require("fs/promises");
10
10
  let path = require("path");
11
- //#region ../types/dist/event-category-BLcNejAE.mjs
11
+ //#region ../types/dist/event-category-Bz24uP1U.mjs
12
12
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
13
13
  EventCategory["SystemBoot"] = "system.boot";
14
14
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -279,6 +279,19 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
279
279
  */
280
280
  EventCategory["DeviceStateChanged"] = "device.state-changed";
281
281
  /**
282
+ * Frame occupancy for a camera CHANGED — a tracked object was gained or
283
+ * lost. Carries `{ deviceId, totalObjects, byClass, zones }`.
284
+ *
285
+ * Emitted only on a change, so a steady scene is silent. It exists so a
286
+ * client can stop polling `zoneAnalytics.getCurrentSnapshot`: that was the
287
+ * one live badge with no push signal at all, and it cost a request every
288
+ * four seconds per visible camera.
289
+ *
290
+ * Like every event it is telemetry and may be dropped ([D8]) — a consumer
291
+ * keeps a slow reconcile rather than trusting it alone.
292
+ */
293
+ EventCategory["ZoneAnalyticsOccupancyChanged"] = "zone-analytics.occupancy-changed";
294
+ /**
282
295
  * Cap event fired by every device that registers the `battery`
283
296
  * capability. Mirrors the cap definition's `onStatusChanged`. Carries
284
297
  * `{ deviceId, status: BatteryStatus }`. Subscribers (alert center,
@@ -7222,35 +7235,22 @@ var ConvertResultSchema = object({
7222
7235
  */
7223
7236
  var RecordingWeekdaySchema = number().int().min(0).max(6);
7224
7237
  var HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
7225
- var RecordingScheduleSchema = discriminatedUnion("kind", [object({ kind: literal("always") }), object({
7226
- kind: literal("timeOfDay"),
7227
- start: string().regex(HHMM),
7228
- end: string().regex(HHMM),
7229
- /** Restrict to these weekdays; omit = every day. */
7230
- days: array(RecordingWeekdaySchema).optional()
7231
- })]);
7232
- var RecordingModeSchema = _enum([
7233
- "continuous",
7234
- "onMotion",
7235
- "onAudioThreshold"
7236
- ]);
7237
7238
  /**
7238
- * First-class, authoritative per-camera storage mode — the explicit choice the
7239
- * UI reads directly (never inferred from `rules`):
7240
- * - `off` — not recording.
7241
- * - `events` — record only around triggers (motion / audio threshold),
7242
- * with pre/post-buffer.
7243
- * - `continuous` — record 24/7 within the schedule.
7239
+ * DERIVED per-camera storage summary — the single field cheap consumers read
7240
+ * (the viewer's status dot, the camera list) instead of walking `bands`:
7241
+ * - `off` — no band covers the camera (or it is disabled).
7242
+ * - `events` — every band records around triggers only.
7243
+ * - `continuous` — at least one band records continuously.
7244
7244
  *
7245
- * `mode` compiles one-way to the internal `rules[]` consumed by the policy
7246
- * engine (see `compileRules`); `rules[]` is never authored directly anymore.
7245
+ * NEVER authored: the recorder stamps it from the authoritative `bands` on
7246
+ * every save (`activeModeForConfig`). Writing it has no effect.
7247
7247
  */
7248
7248
  var RecordingStorageModeSchema = _enum([
7249
7249
  "off",
7250
7250
  "events",
7251
7251
  "continuous"
7252
7252
  ]);
7253
- /** Which detectors trigger an `events`-mode recording. */
7253
+ /** Which detectors trigger an `events`-mode band. */
7254
7254
  var RecordingTriggersSchema = object({
7255
7255
  motion: boolean().optional(),
7256
7256
  audioThresholdDbfs: number().optional()
@@ -7286,18 +7286,6 @@ var RecordingBandSchema = object({
7286
7286
  preBufferSec: number().min(0).optional(),
7287
7287
  postBufferSec: number().min(0).optional()
7288
7288
  });
7289
- var RecordingRuleSchema = object({
7290
- schedule: RecordingScheduleSchema,
7291
- mode: RecordingModeSchema,
7292
- /** Seconds of footage to retain BEFORE a trigger (applied at keep/discard). */
7293
- preBufferSec: number().min(0).default(0),
7294
- /** Keep recording until this many seconds after the last trigger. */
7295
- postBufferSec: number().min(0).default(0),
7296
- /** Each new trigger restarts the post-buffer window. */
7297
- resetTimeoutOnNewEvent: boolean().default(true),
7298
- /** onAudioThreshold only — dBFS level that counts as a trigger. */
7299
- thresholdDbfs: number().optional()
7300
- });
7301
7289
  /**
7302
7290
  * Per-device retention overrides. Every field is optional; an unset or `0`
7303
7291
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7331,40 +7319,28 @@ var ScrubThumbnailPresetSchema = _enum([
7331
7319
  /**
7332
7320
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7333
7321
  *
7334
- * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
7335
- * are its mode-specific parameters. `rules` is a DEPRECATED authoring input kept
7336
- * only for transition + migration (`migrateRulesToMode`); the policy engine
7337
- * consumes the compiled output of `compileRules(config)`, never `rules` directly.
7322
+ * `bands` is the ONLY authored recording intent: what to record, when, and on
7323
+ * which trigger. `mode` is a derived summary the recorder stamps on save; every
7324
+ * other field is a storage knob (profiles, segment length, retention, scrub).
7325
+ *
7326
+ * STRICT on purpose: the legacy authoring surface (`schedule`/`schedules`/
7327
+ * `triggers`/`preBufferSec`/`postBufferSec`/`rules`) was retired 2026-07-30.
7328
+ * A stale caller must fail loudly — silently stripping its legacy intent would
7329
+ * persist a band-less config, i.e. silently stop recording the camera.
7338
7330
  */
7339
7331
  var RecordingConfigSchema = object({
7340
7332
  enabled: boolean(),
7341
- /** Authoritative storage mode. Absent on legacy targets → derived once via
7342
- * `migrateRulesToMode`, then persisted. */
7333
+ /** DERIVED summary of `bands`, stamped by the recorder on every save.
7334
+ * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7343
7335
  mode: RecordingStorageModeSchema.optional(),
7344
7336
  profiles: array(CamProfileSchema).optional(),
7345
7337
  segmentSeconds: number().int().positive().optional(),
7346
- /** Shared recording time-bands for `events` & `continuous` — record only when
7347
- * the wall-clock falls inside one of these windows. Omit or empty = always.
7348
- * `continuous` compiles to one rule per band; `events` to band × trigger. */
7349
- schedules: array(RecordingScheduleSchema).optional(),
7350
- /** Legacy single-band predecessor of `schedules`. Read-compat only — it is
7351
- * normalized into `schedules` on read and never written going forward. (Not
7352
- * tagged `@deprecated`: the normalization paths must read it cast-free.) */
7353
- schedule: RecordingScheduleSchema.optional(),
7354
- /** `events`-mode only — which detectors trigger a recording. */
7355
- triggers: RecordingTriggersSchema.optional(),
7356
- /** `events`-mode only — seconds retained before / after a trigger. */
7357
- preBufferSec: number().min(0).optional(),
7358
- postBufferSec: number().min(0).optional(),
7359
- /** DEPRECATED authoring input; retained for migration/transition. */
7360
- rules: array(RecordingRuleSchema).optional(),
7361
7338
  /**
7362
- * AUTHORITATIVE mode-per-band recording model (recorder). When present it
7363
- * is the single source of truth; the legacy `mode`/`schedules`/`schedule`/
7364
- * `triggers`/`rules` fields above are kept for READ-COMPAT only and are
7365
- * derived into bands once via `migrateConfigToBands`.
7339
+ * AUTHORITATIVE mode-per-band recording model — the single source of truth
7340
+ * the recorder's band engine consumes. An empty array = record nothing;
7341
+ * "off" is the absence of a covering band, never a band value.
7366
7342
  */
7367
- bands: array(RecordingBandSchema).optional(),
7343
+ bands: array(RecordingBandSchema).default([]),
7368
7344
  retention: RecordingRetentionSchema.optional(),
7369
7345
  /**
7370
7346
  * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
@@ -7372,8 +7348,15 @@ var RecordingConfigSchema = object({
7372
7348
  * windows only — existing sheets are immutable, and each window's index
7373
7349
  * carries its own tile dims so mixed-preset history renders correctly.
7374
7350
  */
7375
- scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7376
- });
7351
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional(),
7352
+ /**
7353
+ * OPT-IN thumbnail-strip generation for this camera: every keyframe of the
7354
+ * low recording saved as a JPEG (the fast-drag scrub depth), a derived
7355
+ * cache that eviction reclaims with the footage. Absent/false = no strips
7356
+ * are written and scrub reads exact keyframes at every velocity.
7357
+ */
7358
+ stripsEnabled: boolean().optional()
7359
+ }).strict();
7377
7360
  /**
7378
7361
  * Ops-log — the durable, append-only operations audit shared by the
7379
7362
  * recordings and events management surfaces.
@@ -7392,7 +7375,8 @@ var OpsLogOpSchema = _enum([
7392
7375
  "prune",
7393
7376
  "manual-delete",
7394
7377
  "rescan",
7395
- "retention-run"
7378
+ "retention-run",
7379
+ "relocate"
7396
7380
  ]);
7397
7381
  /** Why the operation ran. */
7398
7382
  var OpsLogReasonSchema = _enum([
@@ -7431,6 +7415,55 @@ var OpsLogQueryInputSchema = object({
7431
7415
  limit: number().int().min(1).max(1e3).optional()
7432
7416
  });
7433
7417
  /**
7418
+ * Entity-relocation job state (storage entity-routing spec, Phase 4).
7419
+ *
7420
+ * One shape shared by the recorder's `relocateFootage` (segments + strips) and
7421
+ * pipeline-analytics' `relocateMedia` (event media blobs) so the admin Data
7422
+ * page renders both movers with one component. Jobs are in-RAM (a restart
7423
+ * forgets them — re-running is safe by construction: copy-if-absent, delete
7424
+ * after verify) and each completed/failed run also lands one durable ops-log
7425
+ * row on the owning addon's surface.
7426
+ */
7427
+ var RelocateJobStateSchema = _enum([
7428
+ "running",
7429
+ "done",
7430
+ "failed",
7431
+ "cancelled"
7432
+ ]);
7433
+ var RelocateJobSchema = object({
7434
+ jobId: string(),
7435
+ state: RelocateJobStateSchema,
7436
+ /** Source location — for media relocation this is informational ('*': rows
7437
+ * move from wherever they are to the target). */
7438
+ fromLocationId: string(),
7439
+ toLocationId: string(),
7440
+ /** Scoped device, or null = every device. */
7441
+ deviceId: number().nullable(),
7442
+ /** What the job moves (owner-addon specific: segments/strips or media). */
7443
+ entities: array(string()),
7444
+ filesMoved: number().int(),
7445
+ bytesMoved: number().int(),
7446
+ /** Total files discovered up front; null while (or when) unknown. */
7447
+ filesTotal: number().int().nullable(),
7448
+ startedAt: number(),
7449
+ finishedAt: number().nullable(),
7450
+ error: string().nullable()
7451
+ });
7452
+ var RelocateFootageInputSchema = object({
7453
+ deviceId: number().optional(),
7454
+ fromLocationId: string(),
7455
+ toLocationId: string(),
7456
+ entities: array(_enum(["segments", "strips"])).optional(),
7457
+ /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7458
+ * never allowed to starve live writers. */
7459
+ throttleMbps: number().min(1).max(1e3).optional()
7460
+ });
7461
+ var RelocateMediaInputSchema = object({
7462
+ deviceId: number().optional(),
7463
+ toLocationId: string(),
7464
+ throttleMbps: number().min(1).max(1e3).optional()
7465
+ });
7466
+ /**
7434
7467
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
7435
7468
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
7436
7469
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -7480,6 +7513,13 @@ var StorageLocationSchema = object({
7480
7513
  nodeId: string().optional(),
7481
7514
  isDefault: boolean().default(false),
7482
7515
  isSystem: boolean().default(false),
7516
+ /** COMPUTED at read time by the orchestrator (statfs of the backing volume
7517
+ * for node-local locations it can reach) — never persisted, absent when the
7518
+ * volume is remote/unreachable. The single capacity truth every UI reads. */
7519
+ capacity: object({
7520
+ totalBytes: number(),
7521
+ availableBytes: number()
7522
+ }).nullable().optional(),
7483
7523
  createdAt: number(),
7484
7524
  updatedAt: number()
7485
7525
  });
@@ -8247,7 +8287,8 @@ var NcTaxonomyEntrySchema = object({
8247
8287
  /** Macro/category parent for grouping ('car' → 'vehicle'); null for a top. */
8248
8288
  parentKind: string().nullable()
8249
8289
  });
8250
- object({
8290
+ /** The complete NC picker taxonomy — three grouped buckets. */
8291
+ var NcTaxonomySchema = object({
8251
8292
  videoClasses: array(NcTaxonomyEntrySchema),
8252
8293
  audioKinds: array(NcTaxonomyEntrySchema),
8253
8294
  labels: array(NcTaxonomyEntrySchema)
@@ -9150,6 +9191,7 @@ function shallowEqual(a, b) {
9150
9191
  for (const k of ak) if (a[k] !== b[k]) return false;
9151
9192
  return true;
9152
9193
  }
9194
+ new Set(["devices", "classes"]);
9153
9195
  /**
9154
9196
  * Shared geometry vocabulary for on-frame shape caps — privacy-mask,
9155
9197
  * motion-zones, and the detection zones/lines editor all speak this one
@@ -9308,6 +9350,29 @@ var NcOccupancyConditionSchema = object({
9308
9350
  count: number().int().min(0).default(1),
9309
9351
  sustainSeconds: number().int().min(0).max(3600).default(15)
9310
9352
  });
9353
+ /**
9354
+ * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
9355
+ *
9356
+ * The values are not symmetric, and deliberately so — the absent value has to
9357
+ * mean exactly what every rule authored before this condition existed already
9358
+ * does:
9359
+ * - `enter` — entries and every NON-crossing record (movement state,
9360
+ * package, sensor). Exits are rejected. **This is the absent behaviour**:
9361
+ * an operator who never asked for exits must not start receiving them.
9362
+ * - `exit` — ONLY an exit crossing. A record that is not a crossing at all
9363
+ * fails closed, because "the car left the drive" is a question about a
9364
+ * boundary, not about a detection.
9365
+ * - `any` — no direction filter; entries, exits and non-crossings alike.
9366
+ *
9367
+ * A rule asking for a direction should normally also scope `zones`, which the
9368
+ * engine evaluates against the crossed zone as well as the current membership
9369
+ * (an exit's membership no longer contains the zone it just left).
9370
+ */
9371
+ var NcCrossingSchema = _enum([
9372
+ "enter",
9373
+ "exit",
9374
+ "any"
9375
+ ]);
9311
9376
  /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
9312
9377
  var NcZoneConditionSchema = object({
9313
9378
  ids: array(string().min(1)).min(1),
@@ -9332,6 +9397,13 @@ var NcConditionsSchema = object({
9332
9397
  /** Veto zones — any hit fails the rule. */
9333
9398
  zonesExclude: array(string().min(1)).optional(),
9334
9399
  /**
9400
+ * Zone-crossing direction. IMMEDIATE only — a crossing is a per-event fact
9401
+ * and a closed track carries none, so a `track-end` rule asking for one
9402
+ * fails closed (use `zones`, which tests `zonesVisited`). ABSENT = `enter`,
9403
+ * which is exactly today's behaviour. See {@link NcCrossingSchema}.
9404
+ */
9405
+ crossing: NcCrossingSchema.optional(),
9406
+ /**
9335
9407
  * Exact (case-insensitive) match on the record's collapsed `label`
9336
9408
  * (identity name / plate text / subclass).
9337
9409
  */
@@ -9466,17 +9538,85 @@ var NcRuleTargetSchema = object({
9466
9538
  * - `keyFrame` — the clean scene frame (no subject box).
9467
9539
  * - `none` — no attachment.
9468
9540
  */
9469
- var NcMediaPolicySchema = object({ attach: _enum([
9470
- "best",
9471
- "best-matching",
9472
- "keyFrame",
9473
- "none"
9474
- ]).default("best") });
9541
+ /**
9542
+ * WHAT THE PICTURE SHOWS — chosen explicitly, because the implicit ladders
9543
+ * conflate it with the selection strategy and betray the request: asking for
9544
+ * the clean scene frame on an object-event owner used to start at
9545
+ * `fullFrameBoxed`, i.e. the annotated frame (2026-07-30).
9546
+ * - `cropped` — the subject, tight. What "who is at the door" wants.
9547
+ * - `full` — the clean scene, no annotation. What "what is going on" wants.
9548
+ * - `boxed` — the scene WITH the detection boxes drawn, for verifying what
9549
+ * the pipeline actually saw.
9550
+ */
9551
+ var NcMediaFrameSchema = _enum([
9552
+ "cropped",
9553
+ "full",
9554
+ "boxed"
9555
+ ]);
9556
+ var NcMediaPolicySchema = object({
9557
+ attach: _enum([
9558
+ "best",
9559
+ "best-matching",
9560
+ "keyFrame",
9561
+ "none"
9562
+ ]).default("best"),
9563
+ /** What the still shows. Absent = `cropped` (the historical `best` shape). */
9564
+ frame: NcMediaFrameSchema.optional(),
9565
+ /** Crop the attached still to the rule's condition-zone bbox (padded) —
9566
+ * "show me the ZONE", not the whole scene or the subject crop. */
9567
+ zoneCrop: boolean().optional(),
9568
+ /**
9569
+ * Also attach a short GIF cut from the stream broker's clip ring around the
9570
+ * event — NOT from the recording, so the camera does not have to be
9571
+ * recording, and the window sits AROUND the moment instead of a segment
9572
+ * behind it. Fail-closed: a window the ring does not cover contributes no
9573
+ * gif, never a failed notification.
9574
+ */
9575
+ gif: boolean().optional(),
9576
+ /**
9577
+ * Also attach a short MP4 CLIP of the same cut. Same source and the same
9578
+ * fail-closed rule as `gif`. Prefer it where the backend takes video
9579
+ * (telegram, discord, zentik, webhook); backends that do not are handled by
9580
+ * the degrade engine, which drops the video and keeps the still.
9581
+ */
9582
+ clip: boolean().optional(),
9583
+ /** Seconds of footage BEFORE / AFTER the event instant. Defaults 3 / 7. */
9584
+ clipPreRollSec: number().int().min(0).max(30).optional(),
9585
+ clipPostRollSec: number().int().min(0).max(30).optional(),
9586
+ /**
9587
+ * Which stream profile the footage is cut from. Absent = the CHEAPEST
9588
+ * assigned profile: a notification is watched on a phone, so the 4K
9589
+ * rendition would burn CPU to produce a file the client downscales anyway.
9590
+ * A profile that is not assigned falls back to the cheapest, and the render
9591
+ * reports which one actually ran.
9592
+ */
9593
+ profile: CamProfileSchema.optional()
9594
+ });
9595
+ /**
9596
+ * Cooldown GRANULARITY over the subject's class — how much a fired
9597
+ * notification suppresses.
9598
+ * - `shared` (default, and the absent value) — one window for the whole
9599
+ * rule/scope: a cat silences the next dog for `cooldownSec`.
9600
+ * - `per-class` — an independent window per detected class, so cat→dog fires
9601
+ * at once and cat→cat still waits.
9602
+ *
9603
+ * AUDIO subjects are ALWAYS per-class regardless of this setting: a scream
9604
+ * must not be swallowed by a bark's window (the precedent this generalizes —
9605
+ * see `cooldownKey` in the rule engine).
9606
+ */
9607
+ var NcThrottleGranularitySchema = _enum(["shared", "per-class"]);
9475
9608
  /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
9476
9609
  var NcThrottleSchema = object({
9477
9610
  cooldownSec: number().int().min(0).max(86400).default(60),
9478
9611
  /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
9479
- scope: _enum(["rule", "rule-device"]).default("rule-device")
9612
+ scope: _enum(["rule", "rule-device"]).default("rule-device"),
9613
+ /**
9614
+ * Class granularity of the cooldown key. Optional rather than defaulted:
9615
+ * a Zod default does NOT run on the addon→addon cap path, so a persisted
9616
+ * rule authored before this field simply carries none — and the engine
9617
+ * reads absent as `shared`, the pre-existing behaviour.
9618
+ */
9619
+ granularity: NcThrottleGranularitySchema.optional()
9480
9620
  });
9481
9621
  /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
9482
9622
  var NcRuleInputSchema = object({
@@ -9485,7 +9625,19 @@ var NcRuleInputSchema = object({
9485
9625
  delivery: NcDeliverySchema,
9486
9626
  conditions: NcConditionsSchema.default({}),
9487
9627
  schedule: NcScheduleSchema.optional(),
9488
- targets: array(NcRuleTargetSchema).min(1),
9628
+ /** May be empty when `targetUsers` addresses at least one user — the
9629
+ * "at least one addressee" invariant is enforced by the provider, because
9630
+ * a cross-field refine here would break `NcRulePatchSchema.partial()`. */
9631
+ targets: array(NcRuleTargetSchema),
9632
+ /**
9633
+ * USERS this rule addresses in addition to `targets` (Phase 4). At fire
9634
+ * time each user fans out to the personal targets they own
9635
+ * (`config.ownerUserId`), filtered by that user's `allowedDevices` for the
9636
+ * firing camera — a user is never notified about a device they cannot open.
9637
+ * Per-user opt-outs (`disabledTargetIds`) still apply to the fanned-out
9638
+ * targets.
9639
+ */
9640
+ targetUsers: array(string()).optional(),
9489
9641
  media: NcMediaPolicySchema.default({ attach: "best" }),
9490
9642
  throttle: NcThrottleSchema.default({
9491
9643
  cooldownSec: 60,
@@ -9572,6 +9724,7 @@ var NcConditionDescriptorSchema = object({
9572
9724
  "schedule",
9573
9725
  "plateMatcher",
9574
9726
  "packagePhase",
9727
+ "crossingSelect",
9575
9728
  "polygonDraw",
9576
9729
  "occupancy"
9577
9730
  ]),
@@ -9698,7 +9851,10 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
9698
9851
  }), object({ results: array(NcTestResultSchema) }), {
9699
9852
  kind: "mutation",
9700
9853
  auth: "admin"
9701
- }), method(object({}), object({ catalog: array(NcConditionDescriptorSchema) })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
9854
+ }), method(object({}), object({
9855
+ catalog: array(NcConditionDescriptorSchema),
9856
+ taxonomy: NcTaxonomySchema.optional()
9857
+ })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
9702
9858
  /**
9703
9859
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
9704
9860
  *
@@ -10707,6 +10863,28 @@ method(object({
10707
10863
  }), object({ success: literal(true) }), {
10708
10864
  kind: "mutation",
10709
10865
  auth: "admin"
10866
+ }), method(object({
10867
+ deviceId: number(),
10868
+ /** Absent = the LOWEST assigned profile — a notification attachment is
10869
+ * watched on a phone, and the cheap rendition is the right default. */
10870
+ profile: CamProfileSchema.optional(),
10871
+ aroundMs: number(),
10872
+ preRollSec: number().min(0).max(20).default(3),
10873
+ postRollSec: number().min(0).max(20).default(5),
10874
+ format: _enum(["gif", "mp4"]).default("gif"),
10875
+ maxWidth: number().int().min(120).max(1920).default(480),
10876
+ /** GIF only — MP4 keeps the source cadence. */
10877
+ fps: number().int().min(1).max(15).default(5)
10878
+ }), object({
10879
+ base64: string(),
10880
+ mime: string(),
10881
+ bytes: number().int(),
10882
+ /** The profile actually rendered (what the default resolved to). */
10883
+ profile: CamProfileSchema,
10884
+ durationMs: number()
10885
+ }), {
10886
+ kind: "mutation",
10887
+ auth: "admin"
10710
10888
  }), method(_void(), array(CameraStreamSchema).readonly()), method(_void(), array(ProfileSlotSchema).readonly()), method(object({ brokerId: string() }), BrokerStatsSchema), method(object({ brokerId: string() }), object({
10711
10889
  probed: boolean(),
10712
10890
  summary: string()
@@ -18835,7 +19013,10 @@ method(object({
18835
19013
  }), method(object({
18836
19014
  deviceId: number(),
18837
19015
  caps: array(string()).readonly().optional()
18838
- }), record(string(), unknown().nullable()));
19016
+ }), record(string(), unknown().nullable())), method(object({
19017
+ deviceIds: array(number()).readonly(),
19018
+ caps: array(string()).readonly().optional()
19019
+ }), record(string(), record(string(), unknown().nullable())));
18839
19020
  method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
18840
19021
  deviceId: number(),
18841
19022
  capName: string()
@@ -19766,6 +19947,36 @@ var TargetKindSchema = object({
19766
19947
  icon: string(),
19767
19948
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
19768
19949
  addonId: string(),
19950
+ /**
19951
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
19952
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
19953
+ * when the addon bundles no icon for that kind — the client then falls back
19954
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
19955
+ *
19956
+ * Root-relative on purpose: it resolves against whatever origin serves a web
19957
+ * client, and a native client joins it onto its own hub base.
19958
+ *
19959
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
19960
+ * field that survived only because the runtime cap-router forwards provider
19961
+ * output verbatim — so every consumer had to re-declare it by hand to stop
19962
+ * its own Zod parse from stripping it, and the whole arrangement would have
19963
+ * broken silently the moment output validation was tightened anywhere.
19964
+ */
19965
+ iconUrl: string().optional(),
19966
+ /**
19967
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
19968
+ *
19969
+ * The server knows this and therefore says it, because the client cannot
19970
+ * safely guess: a React-Native client renders SVG and raster through two
19971
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
19972
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
19973
+ * placeholder glyph for every vector icon while the web build looked fine.
19974
+ *
19975
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
19976
+ * not been updated — a client that cannot determine the type should prefer
19977
+ * its raster path, which is the safe default for an unknown image.
19978
+ */
19979
+ iconMediaType: string().optional(),
19769
19980
  configSchema: ConfigSchemaPassthrough,
19770
19981
  supportsDiscovery: boolean(),
19771
19982
  caps: TargetKindCapsSchema
@@ -20213,6 +20424,29 @@ var MotionEventSchema = object({
20213
20424
  * Absent on legacy rows ⇒ treat as `pipeline`.
20214
20425
  */
20215
20426
  var DetectionSourceSchema = _enum(["pipeline", "onboard"]);
20427
+ /**
20428
+ * The confirmed zone crossing that produced an object event. Present ONLY on
20429
+ * an event emitted BY a crossing (`zone.enter` / `zone.exit`); a movement-state
20430
+ * event (`object.entering` / `leaving` / `stationary` / `loitering`) and an
20431
+ * appearance event carry none, so a rule asking for a direction fails closed
20432
+ * on them.
20433
+ *
20434
+ * Exactly ONE crossing per event: the emitter turns each confirmed crossing
20435
+ * into its own event, so a frame in which a track enters A while leaving B
20436
+ * produces two events with two directions — never one ambiguous row.
20437
+ *
20438
+ * `zoneId` is load-bearing for an EXIT: the event's `zones` list is the
20439
+ * membership the box has NOW, and by definition it no longer contains the zone
20440
+ * that was just left. Without the id here, a zone-scoped rule could never match
20441
+ * the exit it asked for.
20442
+ */
20443
+ var ZoneCrossingSchema = object({
20444
+ direction: _enum(["enter", "exit"]),
20445
+ /** Admin zone id crossed. */
20446
+ zoneId: string(),
20447
+ /** Zone display name at crossing time (falls back to the id). */
20448
+ zoneName: string().optional()
20449
+ });
20216
20450
  var ObjectEventSchema = object({
20217
20451
  ...BaseEventFields,
20218
20452
  kind: literal("object"),
@@ -20239,6 +20473,12 @@ var ObjectEventSchema = object({
20239
20473
  zones: array(string()).readonly().optional(),
20240
20474
  /** Omitted in slim projection. */
20241
20475
  state: TrackStateSchema.optional(),
20476
+ /**
20477
+ * The zone crossing this event IS, when it is one. Absent on every other
20478
+ * event kind (movement state, appearance, package) — see
20479
+ * {@link ZoneCrossingSchema}. Omitted in slim projection.
20480
+ */
20481
+ zoneCrossing: ZoneCrossingSchema.optional(),
20242
20482
  /** Detection-frame dimensions in pixels — let consumers normalize the
20243
20483
  * pixel-space `bbox` onto a displayed image. Omitted in slim projection. */
20244
20484
  frameWidth: number().optional(),
@@ -20497,6 +20737,15 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20497
20737
  }), method(object({ deviceId: number() }), EventPruneCountsSchema, {
20498
20738
  kind: "mutation",
20499
20739
  auth: "admin"
20740
+ }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
20741
+ kind: "mutation",
20742
+ auth: "admin"
20743
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
20744
+ kind: "query",
20745
+ auth: "admin"
20746
+ }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
20747
+ kind: "mutation",
20748
+ auth: "admin"
20500
20749
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
20501
20750
  kind: "query",
20502
20751
  auth: "admin"
@@ -22976,6 +23225,17 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
22976
23225
  */
22977
23226
  priority: number()
22978
23227
  })).readonly() });
23228
+ /**
23229
+ * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
23230
+ * AUTO (resolved from the candidate ranking at send time); `resolved` reports
23231
+ * what AUTO currently picks, so the UI can show the effective value either way.
23232
+ */
23233
+ var NotificationEndpointSchema = object({
23234
+ /** The operator's explicit choice, or null for AUTO. */
23235
+ baseUrl: string().nullable(),
23236
+ /** What the ranking currently resolves to (null when nothing is reachable). */
23237
+ resolved: string().nullable()
23238
+ });
22979
23239
  var AllowedAddressesSchema = object({
22980
23240
  /**
22981
23241
  * Allowlist of interface addresses operators have explicitly opted
@@ -22998,7 +23258,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22998
23258
  * to avoid mixed-content blocks in the browser. The public
22999
23259
  * tunnel always emits `https://` regardless. */
23000
23260
  scheme: _enum(["http", "https"]).optional()
23001
- }), GetConnectionEndpointsResultSchema), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
23261
+ }), 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" });
23002
23262
  /**
23003
23263
  * mesh-network — collection cap for mesh-VPN providers.
23004
23264
  *
@@ -23797,7 +24057,12 @@ var RecordingDeviceUsageSchema = object({
23797
24057
  var RecordingLocationUsageSchema = object({
23798
24058
  /** StorageLocation id; null for the legacy/degraded single-root fallback. */
23799
24059
  locationId: string().nullable(),
23800
- /** Bytes of recordings stored on this location. */
24060
+ /** Every location id sharing this row's PHYSICAL volume (aliases). One row
24061
+ * is emitted per physical disk (2026-07-29): two locations on one root
24062
+ * previously rendered as two identical "disks" with a nonsensical used
24063
+ * split — hydrate attribution across aliases is arbitrary by nature. */
24064
+ locationIds: array(string()).optional(),
24065
+ /** Bytes of recordings stored on this PHYSICAL volume (all aliases). */
23801
24066
  usedBytes: number(),
23802
24067
  /** Free bytes on the location's volume; null when capacity is unknown (remote). */
23803
24068
  availableBytes: number().nullable(),
@@ -23909,6 +24174,44 @@ method(object({
23909
24174
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
23910
24175
  kind: "query",
23911
24176
  auth: "admin"
24177
+ }), method(object({
24178
+ deviceId: number(),
24179
+ aroundMs: number(),
24180
+ preRollSec: number().min(0).max(30).default(2),
24181
+ postRollSec: number().min(0).max(30).default(5),
24182
+ maxWidth: number().int().min(120).max(1280).default(480),
24183
+ fps: number().int().min(1).max(15).default(5)
24184
+ }), object({
24185
+ gifBase64: string(),
24186
+ fromMs: number(),
24187
+ toMs: number()
24188
+ }), {
24189
+ kind: "mutation",
24190
+ auth: "admin"
24191
+ }), method(object({
24192
+ deviceId: number(),
24193
+ aroundMs: number(),
24194
+ preRollSec: number().min(0).max(30).default(3),
24195
+ postRollSec: number().min(0).max(30).default(7),
24196
+ maxWidth: number().int().min(160).max(1920).default(640)
24197
+ }), object({
24198
+ clipBase64: string(),
24199
+ mime: string(),
24200
+ fromMs: number(),
24201
+ toMs: number(),
24202
+ bytes: number().int()
24203
+ }), {
24204
+ kind: "mutation",
24205
+ auth: "admin"
24206
+ }), method(RelocateFootageInputSchema, object({ jobId: string() }), {
24207
+ kind: "mutation",
24208
+ auth: "admin"
24209
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
24210
+ kind: "query",
24211
+ auth: "admin"
24212
+ }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
24213
+ kind: "mutation",
24214
+ auth: "admin"
23912
24215
  });
23913
24216
  /**
23914
24217
  * `recordingExport` cap — render a footage time range into a single downloadable
@@ -25500,6 +25803,12 @@ Object.freeze({
25500
25803
  addonId: null,
25501
25804
  access: "view"
25502
25805
  },
25806
+ "deviceManager.getDeviceStatusAggregateBatch": {
25807
+ capName: "device-manager",
25808
+ capScope: "system",
25809
+ addonId: null,
25810
+ access: "view"
25811
+ },
25503
25812
  "deviceManager.getLinkedDevices": {
25504
25813
  capName: "device-manager",
25505
25814
  capScope: "system",
@@ -26394,6 +26703,12 @@ Object.freeze({
26394
26703
  addonId: null,
26395
26704
  access: "view"
26396
26705
  },
26706
+ "localNetwork.getNotificationEndpoint": {
26707
+ capName: "local-network",
26708
+ capScope: "system",
26709
+ addonId: null,
26710
+ access: "view"
26711
+ },
26397
26712
  "localNetwork.getPreferred": {
26398
26713
  capName: "local-network",
26399
26714
  capScope: "system",
@@ -26418,6 +26733,12 @@ Object.freeze({
26418
26733
  addonId: null,
26419
26734
  access: "create"
26420
26735
  },
26736
+ "localNetwork.setNotificationEndpoint": {
26737
+ capName: "local-network",
26738
+ capScope: "system",
26739
+ addonId: null,
26740
+ access: "create"
26741
+ },
26421
26742
  "lockControl.lock": {
26422
26743
  capName: "lock-control",
26423
26744
  capScope: "device",
@@ -27060,6 +27381,12 @@ Object.freeze({
27060
27381
  addonId: null,
27061
27382
  access: "create"
27062
27383
  },
27384
+ "pipelineAnalytics.cancelMediaRelocate": {
27385
+ capName: "pipeline-analytics",
27386
+ capScope: "device",
27387
+ addonId: null,
27388
+ access: "create"
27389
+ },
27063
27390
  "pipelineAnalytics.clearTracks": {
27064
27391
  capName: "pipeline-analytics",
27065
27392
  capScope: "device",
@@ -27114,6 +27441,12 @@ Object.freeze({
27114
27441
  addonId: null,
27115
27442
  access: "view"
27116
27443
  },
27444
+ "pipelineAnalytics.getMediaRelocateStatus": {
27445
+ capName: "pipeline-analytics",
27446
+ capScope: "device",
27447
+ addonId: null,
27448
+ access: "view"
27449
+ },
27117
27450
  "pipelineAnalytics.getMotionEvents": {
27118
27451
  capName: "pipeline-analytics",
27119
27452
  capScope: "device",
@@ -27186,6 +27519,12 @@ Object.freeze({
27186
27519
  addonId: null,
27187
27520
  access: "create"
27188
27521
  },
27522
+ "pipelineAnalytics.relocateMedia": {
27523
+ capName: "pipeline-analytics",
27524
+ capScope: "device",
27525
+ addonId: null,
27526
+ access: "create"
27527
+ },
27189
27528
  "pipelineAnalytics.searchObjectEvents": {
27190
27529
  capName: "pipeline-analytics",
27191
27530
  capScope: "device",
@@ -27948,6 +28287,12 @@ Object.freeze({
27948
28287
  addonId: null,
27949
28288
  access: "create"
27950
28289
  },
28290
+ "recording.cancelRelocate": {
28291
+ capName: "recording",
28292
+ capScope: "system",
28293
+ addonId: null,
28294
+ access: "create"
28295
+ },
27951
28296
  "recording.deleteFootprint": {
27952
28297
  capName: "recording",
27953
28298
  capScope: "system",
@@ -27978,6 +28323,12 @@ Object.freeze({
27978
28323
  addonId: null,
27979
28324
  access: "view"
27980
28325
  },
28326
+ "recording.getRelocateStatus": {
28327
+ capName: "recording",
28328
+ capScope: "system",
28329
+ addonId: null,
28330
+ access: "view"
28331
+ },
27981
28332
  "recording.getStorageUsage": {
27982
28333
  capName: "recording",
27983
28334
  capScope: "system",
@@ -28008,6 +28359,24 @@ Object.freeze({
28008
28359
  addonId: null,
28009
28360
  access: "view"
28010
28361
  },
28362
+ "recording.relocateFootage": {
28363
+ capName: "recording",
28364
+ capScope: "system",
28365
+ addonId: null,
28366
+ access: "create"
28367
+ },
28368
+ "recording.renderClip": {
28369
+ capName: "recording",
28370
+ capScope: "system",
28371
+ addonId: null,
28372
+ access: "create"
28373
+ },
28374
+ "recording.renderGif": {
28375
+ capName: "recording",
28376
+ capScope: "system",
28377
+ addonId: null,
28378
+ access: "create"
28379
+ },
28011
28380
  "recording.rescanStorage": {
28012
28381
  capName: "recording",
28013
28382
  capScope: "system",
@@ -28602,6 +28971,12 @@ Object.freeze({
28602
28971
  addonId: null,
28603
28972
  access: "create"
28604
28973
  },
28974
+ "streamBroker.renderPreBufferClip": {
28975
+ capName: "stream-broker",
28976
+ capScope: "system",
28977
+ addonId: null,
28978
+ access: "create"
28979
+ },
28605
28980
  "streamBroker.restartProfile": {
28606
28981
  capName: "stream-broker",
28607
28982
  capScope: "system",