@camstack/addon-provider-homematic 1.2.6 → 1.2.8

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 +463 -76
  2. package/dist/addon.mjs +463 -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()
@@ -13837,6 +14015,18 @@ var motionCapability = {
13837
14015
  name: "motion",
13838
14016
  scope: "device",
13839
14017
  mode: "singleton",
14018
+ /**
14019
+ * Providers register per-device natives via `ctx.registerNativeCap`
14020
+ * (Hikvision/Reolink/Amcrest/Wyze/HA/Homematic/Alexa/Matter) — there is
14021
+ * NO system singleton provider. Without this flag `resolveCapMount`
14022
+ * derived `{ kind: 'singleton' }`, so `motion.getStatus`/`isDetected`
14023
+ * resolved via `registry.getSingleton('motion')` (always null) and every
14024
+ * call 412'd "provider not available" while bindings listed a live
14025
+ * `motion` native (2026-08-02). The flag routes the router through
14026
+ * `requireDeviceScoped` → `getProviderForDevice`, like `motion-trigger`,
14027
+ * `snapshot` and every other per-device native cap.
14028
+ */
14029
+ deviceNative: true,
13840
14030
  deviceTypes: [DeviceType.Camera, DeviceType.Sensor],
13841
14031
  methods: {
13842
14032
  /**
@@ -18835,7 +19025,10 @@ method(object({
18835
19025
  }), method(object({
18836
19026
  deviceId: number(),
18837
19027
  caps: array(string()).readonly().optional()
18838
- }), record(string(), unknown().nullable()));
19028
+ }), record(string(), unknown().nullable())), method(object({
19029
+ deviceIds: array(number()).readonly(),
19030
+ caps: array(string()).readonly().optional()
19031
+ }), record(string(), record(string(), unknown().nullable())));
18839
19032
  method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
18840
19033
  deviceId: number(),
18841
19034
  capName: string()
@@ -19766,6 +19959,36 @@ var TargetKindSchema = object({
19766
19959
  icon: string(),
19767
19960
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
19768
19961
  addonId: string(),
19962
+ /**
19963
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
19964
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
19965
+ * when the addon bundles no icon for that kind — the client then falls back
19966
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
19967
+ *
19968
+ * Root-relative on purpose: it resolves against whatever origin serves a web
19969
+ * client, and a native client joins it onto its own hub base.
19970
+ *
19971
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
19972
+ * field that survived only because the runtime cap-router forwards provider
19973
+ * output verbatim — so every consumer had to re-declare it by hand to stop
19974
+ * its own Zod parse from stripping it, and the whole arrangement would have
19975
+ * broken silently the moment output validation was tightened anywhere.
19976
+ */
19977
+ iconUrl: string().optional(),
19978
+ /**
19979
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
19980
+ *
19981
+ * The server knows this and therefore says it, because the client cannot
19982
+ * safely guess: a React-Native client renders SVG and raster through two
19983
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
19984
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
19985
+ * placeholder glyph for every vector icon while the web build looked fine.
19986
+ *
19987
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
19988
+ * not been updated — a client that cannot determine the type should prefer
19989
+ * its raster path, which is the safe default for an unknown image.
19990
+ */
19991
+ iconMediaType: string().optional(),
19769
19992
  configSchema: ConfigSchemaPassthrough,
19770
19993
  supportsDiscovery: boolean(),
19771
19994
  caps: TargetKindCapsSchema
@@ -20213,6 +20436,29 @@ var MotionEventSchema = object({
20213
20436
  * Absent on legacy rows ⇒ treat as `pipeline`.
20214
20437
  */
20215
20438
  var DetectionSourceSchema = _enum(["pipeline", "onboard"]);
20439
+ /**
20440
+ * The confirmed zone crossing that produced an object event. Present ONLY on
20441
+ * an event emitted BY a crossing (`zone.enter` / `zone.exit`); a movement-state
20442
+ * event (`object.entering` / `leaving` / `stationary` / `loitering`) and an
20443
+ * appearance event carry none, so a rule asking for a direction fails closed
20444
+ * on them.
20445
+ *
20446
+ * Exactly ONE crossing per event: the emitter turns each confirmed crossing
20447
+ * into its own event, so a frame in which a track enters A while leaving B
20448
+ * produces two events with two directions — never one ambiguous row.
20449
+ *
20450
+ * `zoneId` is load-bearing for an EXIT: the event's `zones` list is the
20451
+ * membership the box has NOW, and by definition it no longer contains the zone
20452
+ * that was just left. Without the id here, a zone-scoped rule could never match
20453
+ * the exit it asked for.
20454
+ */
20455
+ var ZoneCrossingSchema = object({
20456
+ direction: _enum(["enter", "exit"]),
20457
+ /** Admin zone id crossed. */
20458
+ zoneId: string(),
20459
+ /** Zone display name at crossing time (falls back to the id). */
20460
+ zoneName: string().optional()
20461
+ });
20216
20462
  var ObjectEventSchema = object({
20217
20463
  ...BaseEventFields,
20218
20464
  kind: literal("object"),
@@ -20239,6 +20485,12 @@ var ObjectEventSchema = object({
20239
20485
  zones: array(string()).readonly().optional(),
20240
20486
  /** Omitted in slim projection. */
20241
20487
  state: TrackStateSchema.optional(),
20488
+ /**
20489
+ * The zone crossing this event IS, when it is one. Absent on every other
20490
+ * event kind (movement state, appearance, package) — see
20491
+ * {@link ZoneCrossingSchema}. Omitted in slim projection.
20492
+ */
20493
+ zoneCrossing: ZoneCrossingSchema.optional(),
20242
20494
  /** Detection-frame dimensions in pixels — let consumers normalize the
20243
20495
  * pixel-space `bbox` onto a displayed image. Omitted in slim projection. */
20244
20496
  frameWidth: number().optional(),
@@ -20497,6 +20749,15 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20497
20749
  }), method(object({ deviceId: number() }), EventPruneCountsSchema, {
20498
20750
  kind: "mutation",
20499
20751
  auth: "admin"
20752
+ }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
20753
+ kind: "mutation",
20754
+ auth: "admin"
20755
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
20756
+ kind: "query",
20757
+ auth: "admin"
20758
+ }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
20759
+ kind: "mutation",
20760
+ auth: "admin"
20500
20761
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
20501
20762
  kind: "query",
20502
20763
  auth: "admin"
@@ -22976,6 +23237,17 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
22976
23237
  */
22977
23238
  priority: number()
22978
23239
  })).readonly() });
23240
+ /**
23241
+ * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
23242
+ * AUTO (resolved from the candidate ranking at send time); `resolved` reports
23243
+ * what AUTO currently picks, so the UI can show the effective value either way.
23244
+ */
23245
+ var NotificationEndpointSchema = object({
23246
+ /** The operator's explicit choice, or null for AUTO. */
23247
+ baseUrl: string().nullable(),
23248
+ /** What the ranking currently resolves to (null when nothing is reachable). */
23249
+ resolved: string().nullable()
23250
+ });
22979
23251
  var AllowedAddressesSchema = object({
22980
23252
  /**
22981
23253
  * Allowlist of interface addresses operators have explicitly opted
@@ -22998,7 +23270,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22998
23270
  * to avoid mixed-content blocks in the browser. The public
22999
23271
  * tunnel always emits `https://` regardless. */
23000
23272
  scheme: _enum(["http", "https"]).optional()
23001
- }), GetConnectionEndpointsResultSchema), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
23273
+ }), 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
23274
  /**
23003
23275
  * mesh-network — collection cap for mesh-VPN providers.
23004
23276
  *
@@ -23797,7 +24069,12 @@ var RecordingDeviceUsageSchema = object({
23797
24069
  var RecordingLocationUsageSchema = object({
23798
24070
  /** StorageLocation id; null for the legacy/degraded single-root fallback. */
23799
24071
  locationId: string().nullable(),
23800
- /** Bytes of recordings stored on this location. */
24072
+ /** Every location id sharing this row's PHYSICAL volume (aliases). One row
24073
+ * is emitted per physical disk (2026-07-29): two locations on one root
24074
+ * previously rendered as two identical "disks" with a nonsensical used
24075
+ * split — hydrate attribution across aliases is arbitrary by nature. */
24076
+ locationIds: array(string()).optional(),
24077
+ /** Bytes of recordings stored on this PHYSICAL volume (all aliases). */
23801
24078
  usedBytes: number(),
23802
24079
  /** Free bytes on the location's volume; null when capacity is unknown (remote). */
23803
24080
  availableBytes: number().nullable(),
@@ -23909,6 +24186,44 @@ method(object({
23909
24186
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
23910
24187
  kind: "query",
23911
24188
  auth: "admin"
24189
+ }), method(object({
24190
+ deviceId: number(),
24191
+ aroundMs: number(),
24192
+ preRollSec: number().min(0).max(30).default(2),
24193
+ postRollSec: number().min(0).max(30).default(5),
24194
+ maxWidth: number().int().min(120).max(1280).default(480),
24195
+ fps: number().int().min(1).max(15).default(5)
24196
+ }), object({
24197
+ gifBase64: string(),
24198
+ fromMs: number(),
24199
+ toMs: number()
24200
+ }), {
24201
+ kind: "mutation",
24202
+ auth: "admin"
24203
+ }), method(object({
24204
+ deviceId: number(),
24205
+ aroundMs: number(),
24206
+ preRollSec: number().min(0).max(30).default(3),
24207
+ postRollSec: number().min(0).max(30).default(7),
24208
+ maxWidth: number().int().min(160).max(1920).default(640)
24209
+ }), object({
24210
+ clipBase64: string(),
24211
+ mime: string(),
24212
+ fromMs: number(),
24213
+ toMs: number(),
24214
+ bytes: number().int()
24215
+ }), {
24216
+ kind: "mutation",
24217
+ auth: "admin"
24218
+ }), method(RelocateFootageInputSchema, object({ jobId: string() }), {
24219
+ kind: "mutation",
24220
+ auth: "admin"
24221
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
24222
+ kind: "query",
24223
+ auth: "admin"
24224
+ }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
24225
+ kind: "mutation",
24226
+ auth: "admin"
23912
24227
  });
23913
24228
  /**
23914
24229
  * `recordingExport` cap — render a footage time range into a single downloadable
@@ -25500,6 +25815,12 @@ Object.freeze({
25500
25815
  addonId: null,
25501
25816
  access: "view"
25502
25817
  },
25818
+ "deviceManager.getDeviceStatusAggregateBatch": {
25819
+ capName: "device-manager",
25820
+ capScope: "system",
25821
+ addonId: null,
25822
+ access: "view"
25823
+ },
25503
25824
  "deviceManager.getLinkedDevices": {
25504
25825
  capName: "device-manager",
25505
25826
  capScope: "system",
@@ -26394,6 +26715,12 @@ Object.freeze({
26394
26715
  addonId: null,
26395
26716
  access: "view"
26396
26717
  },
26718
+ "localNetwork.getNotificationEndpoint": {
26719
+ capName: "local-network",
26720
+ capScope: "system",
26721
+ addonId: null,
26722
+ access: "view"
26723
+ },
26397
26724
  "localNetwork.getPreferred": {
26398
26725
  capName: "local-network",
26399
26726
  capScope: "system",
@@ -26418,6 +26745,12 @@ Object.freeze({
26418
26745
  addonId: null,
26419
26746
  access: "create"
26420
26747
  },
26748
+ "localNetwork.setNotificationEndpoint": {
26749
+ capName: "local-network",
26750
+ capScope: "system",
26751
+ addonId: null,
26752
+ access: "create"
26753
+ },
26421
26754
  "lockControl.lock": {
26422
26755
  capName: "lock-control",
26423
26756
  capScope: "device",
@@ -27060,6 +27393,12 @@ Object.freeze({
27060
27393
  addonId: null,
27061
27394
  access: "create"
27062
27395
  },
27396
+ "pipelineAnalytics.cancelMediaRelocate": {
27397
+ capName: "pipeline-analytics",
27398
+ capScope: "device",
27399
+ addonId: null,
27400
+ access: "create"
27401
+ },
27063
27402
  "pipelineAnalytics.clearTracks": {
27064
27403
  capName: "pipeline-analytics",
27065
27404
  capScope: "device",
@@ -27114,6 +27453,12 @@ Object.freeze({
27114
27453
  addonId: null,
27115
27454
  access: "view"
27116
27455
  },
27456
+ "pipelineAnalytics.getMediaRelocateStatus": {
27457
+ capName: "pipeline-analytics",
27458
+ capScope: "device",
27459
+ addonId: null,
27460
+ access: "view"
27461
+ },
27117
27462
  "pipelineAnalytics.getMotionEvents": {
27118
27463
  capName: "pipeline-analytics",
27119
27464
  capScope: "device",
@@ -27186,6 +27531,12 @@ Object.freeze({
27186
27531
  addonId: null,
27187
27532
  access: "create"
27188
27533
  },
27534
+ "pipelineAnalytics.relocateMedia": {
27535
+ capName: "pipeline-analytics",
27536
+ capScope: "device",
27537
+ addonId: null,
27538
+ access: "create"
27539
+ },
27189
27540
  "pipelineAnalytics.searchObjectEvents": {
27190
27541
  capName: "pipeline-analytics",
27191
27542
  capScope: "device",
@@ -27948,6 +28299,12 @@ Object.freeze({
27948
28299
  addonId: null,
27949
28300
  access: "create"
27950
28301
  },
28302
+ "recording.cancelRelocate": {
28303
+ capName: "recording",
28304
+ capScope: "system",
28305
+ addonId: null,
28306
+ access: "create"
28307
+ },
27951
28308
  "recording.deleteFootprint": {
27952
28309
  capName: "recording",
27953
28310
  capScope: "system",
@@ -27978,6 +28335,12 @@ Object.freeze({
27978
28335
  addonId: null,
27979
28336
  access: "view"
27980
28337
  },
28338
+ "recording.getRelocateStatus": {
28339
+ capName: "recording",
28340
+ capScope: "system",
28341
+ addonId: null,
28342
+ access: "view"
28343
+ },
27981
28344
  "recording.getStorageUsage": {
27982
28345
  capName: "recording",
27983
28346
  capScope: "system",
@@ -28008,6 +28371,24 @@ Object.freeze({
28008
28371
  addonId: null,
28009
28372
  access: "view"
28010
28373
  },
28374
+ "recording.relocateFootage": {
28375
+ capName: "recording",
28376
+ capScope: "system",
28377
+ addonId: null,
28378
+ access: "create"
28379
+ },
28380
+ "recording.renderClip": {
28381
+ capName: "recording",
28382
+ capScope: "system",
28383
+ addonId: null,
28384
+ access: "create"
28385
+ },
28386
+ "recording.renderGif": {
28387
+ capName: "recording",
28388
+ capScope: "system",
28389
+ addonId: null,
28390
+ access: "create"
28391
+ },
28011
28392
  "recording.rescanStorage": {
28012
28393
  capName: "recording",
28013
28394
  capScope: "system",
@@ -28602,6 +28983,12 @@ Object.freeze({
28602
28983
  addonId: null,
28603
28984
  access: "create"
28604
28985
  },
28986
+ "streamBroker.renderPreBufferClip": {
28987
+ capName: "stream-broker",
28988
+ capScope: "system",
28989
+ addonId: null,
28990
+ access: "create"
28991
+ },
28605
28992
  "streamBroker.restartProfile": {
28606
28993
  capName: "stream-broker",
28607
28994
  capScope: "system",