@camstack/addon-provider-homematic 1.2.21 → 1.2.22

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 +179 -44
  2. package/dist/addon.mjs +179 -44
  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-Bxo5yJjt.mjs
11
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
12
12
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
13
13
  EventCategory["SystemBoot"] = "system.boot";
14
14
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -25,9 +25,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
25
25
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
26
26
  /**
27
27
  * A newer addon or server-root package version was found by the
28
- * authoritative registry check. Emitted once per
29
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
30
- * polling of the same result is deduplicated by the checker.
28
+ * authoritative registry check. Emitted once when any observed
29
+ * `latestVersion` changes (or a package/node first appears behind);
30
+ * the payload carries the full currently-available list. Repeated
31
+ * polling of the same latests is silent.
31
32
  */
32
33
  EventCategory["UpdateAvailable"] = "update.available";
33
34
  /**
@@ -46,6 +47,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
46
47
  EventCategory["AddonInstalled"] = "addon.installed";
47
48
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
48
49
  EventCategory["AddonCrashed"] = "addon.crashed";
50
+ /**
51
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
52
+ *
53
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
54
+ * respawned"); this is the one that never resolves itself. Emitted exactly
55
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
56
+ * addons it hosted and the crash count that tripped the breaker.
57
+ *
58
+ * It exists because a runner marked terminally `failed` used to be SILENT:
59
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
60
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
61
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
62
+ * operator's phone told him. The Notification Center's system-event intake
63
+ * consumes this category and maps it to the `addon-crash-loop` kind.
64
+ */
65
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
49
66
  EventCategory["AddonError"] = "addon.error";
50
67
  EventCategory["AddonPageReady"] = "addon.page-ready";
51
68
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -7557,6 +7574,10 @@ var RecordingBandSchema = object({
7557
7574
  preBufferSec: number().min(0).optional(),
7558
7575
  postBufferSec: number().min(0).optional()
7559
7576
  });
7577
+ ({
7578
+ preBufferSec: 10,
7579
+ postBufferSec: 30
7580
+ }).postBufferSec * 1e3;
7560
7581
  /**
7561
7582
  * Per-device retention overrides. Every field is optional; an unset or `0`
7562
7583
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7604,6 +7625,12 @@ var RecordingConfigSchema = object({
7604
7625
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
7605
7626
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7606
7627
  mode: RecordingStorageModeSchema.optional(),
7628
+ /**
7629
+ * Which assigned broker slots to record. Absent / empty = {@link
7630
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
7631
+ * camera's currently assigned slots — never `mid` unless the operator
7632
+ * picks it, and never a slot the broker has not assigned.
7633
+ */
7607
7634
  profiles: array(CamProfileSchema).optional(),
7608
7635
  segmentSeconds: number().int().positive().optional(),
7609
7636
  /**
@@ -7684,7 +7711,11 @@ var RelocateFootageInputSchema = object({
7684
7711
  profiles: array(string()).optional(),
7685
7712
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7686
7713
  * never allowed to starve live writers. */
7687
- throttleMbps: number().min(1).max(1e3).optional()
7714
+ throttleMbps: number().min(1).max(1e3).optional(),
7715
+ /** Move only segments whose startMs is >= this. Absent = the whole source
7716
+ * pile. Used when a full drain is too expensive and the operator only
7717
+ * wants the recent window on the new disk. */
7718
+ sinceMs: number().int().optional()
7688
7719
  });
7689
7720
  /** Internal, lease-scoped participant operation. It is intentionally separate
7690
7721
  * from persistent recording settings: a migration never changes
@@ -14615,6 +14646,7 @@ var NcSystemEventKindSchema = _enum([
14615
14646
  "node-offline",
14616
14647
  "node-inference-unavailable",
14617
14648
  "detection-blind",
14649
+ "addon-crash-loop",
14618
14650
  "addon-update-available",
14619
14651
  "server-update-available",
14620
14652
  "alarm-triggered",
@@ -14622,6 +14654,9 @@ var NcSystemEventKindSchema = _enum([
14622
14654
  "alarm-disarmed",
14623
14655
  "alarm-arming",
14624
14656
  "alarm-arm-refused",
14657
+ "addon-updated",
14658
+ "server-updated",
14659
+ "export-completed",
14625
14660
  "camera-online",
14626
14661
  "camera-offline",
14627
14662
  "camera-disabled",
@@ -16515,13 +16550,19 @@ var RetrainStatusSchema = _enum([
16515
16550
  * "never marked" from "already trained" must read `retrainStatus`.
16516
16551
  *
16517
16552
  * `debug` does NOT pin; it is attention, not durability.
16553
+ *
16554
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16555
+ * A favourited track is skipped by retention the same way `staging` is, but
16556
+ * it does not enter `none|staging|trained` and has no staging budget.
16518
16557
  */
16519
16558
  var TrackFlagFields = {
16520
16559
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16521
16560
  * `'staging'`. */
16522
16561
  markForTrain: boolean().optional(),
16523
16562
  /** Operator marked this track for diagnostic attention. */
16524
- debug: boolean().optional()
16563
+ debug: boolean().optional(),
16564
+ /** Operator favourited this track. Pins it against pruning. */
16565
+ favourited: boolean().optional()
16525
16566
  };
16526
16567
  /**
16527
16568
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16546,6 +16587,7 @@ var TrackFlagsSchema = object({
16546
16587
  trackId: string(),
16547
16588
  markForTrain: boolean(),
16548
16589
  debug: boolean(),
16590
+ favourited: boolean(),
16549
16591
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16550
16592
  * a track row) because this shape is only ever produced by the write body,
16551
16593
  * which always knows it — and a surface that has just written needs to render
@@ -17178,6 +17220,12 @@ var TrackCascadeCountsSchema = object({
17178
17220
  /** Per-track CLIP search vectors removed (best-effort). */
17179
17221
  embeddings: number().int()
17180
17222
  });
17223
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17224
+ var DiskReconcileCountsSchema = object({
17225
+ mediaDropped: number().int(),
17226
+ tracks: number().int(),
17227
+ events: number().int()
17228
+ });
17181
17229
  /** Event-store footprint for one camera. */
17182
17230
  var EventStoreDeviceFootprintSchema = object({
17183
17231
  deviceId: number(),
@@ -17354,6 +17402,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17354
17402
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17355
17403
  kind: "mutation",
17356
17404
  auth: "admin"
17405
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17406
+ kind: "mutation",
17407
+ auth: "admin"
17357
17408
  }), method(object({
17358
17409
  deviceId: number(),
17359
17410
  trackIds: array(string()).min(1)
@@ -18904,6 +18955,24 @@ var CameraStatusDegradationSchema = object({
18904
18955
  *
18905
18956
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
18906
18957
  */
18958
+ var DiskReconcileJobSchema = object({
18959
+ state: _enum([
18960
+ "idle",
18961
+ "running",
18962
+ "done",
18963
+ "error"
18964
+ ]),
18965
+ total: number().int().nonnegative(),
18966
+ completed: number().int().nonnegative(),
18967
+ currentDeviceId: number().int().nullable(),
18968
+ failed: array(number().int()).readonly(),
18969
+ mediaDropped: number().int().nonnegative(),
18970
+ tracks: number().int().nonnegative(),
18971
+ events: number().int().nonnegative(),
18972
+ startedAtMs: number().int().nullable(),
18973
+ finishedAtMs: number().int().nullable(),
18974
+ error: string().nullable()
18975
+ });
18907
18976
  var CameraStatusSchema = object({
18908
18977
  deviceId: number(),
18909
18978
  assignment: CameraAssignmentStatusSchema,
@@ -19135,7 +19204,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19135
19204
  }), CameraSwitchGroupSchema, {
19136
19205
  kind: "mutation",
19137
19206
  auth: "admin"
19138
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19207
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
19208
+ kind: "mutation",
19209
+ auth: "admin"
19210
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19139
19211
  name: string(),
19140
19212
  description: string().optional(),
19141
19213
  config: CameraPipelineConfigSchema
@@ -20208,20 +20280,6 @@ var VectorStatsResultSchema = object({
20208
20280
  exact: boolean()
20209
20281
  });
20210
20282
  method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
20211
- /**
20212
- * `videoclips` — the unified, navigable-clip surface for a camera.
20213
- *
20214
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
20215
- * provider per device, substitutable. The DEFAULT provider (registered by
20216
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
20217
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
20218
- * is a time-WINDOW over existing footage, never a separate file. A camera that
20219
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
20220
- * wrapper provider for its device and serve its own clip catalog + URLs.
20221
- *
20222
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
20223
- * temporal overlap, so the API never decides `continuous` vs `events`.
20224
- */
20225
20283
  var ClipSchema = object({
20226
20284
  /** Opaque, provider-namespaced id. The default provider encodes the time
20227
20285
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -20238,8 +20296,57 @@ var ClipSchema = object({
20238
20296
  startMs: number(),
20239
20297
  endMs: number()
20240
20298
  }),
20241
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
20242
- thumbnail: string().optional()
20299
+ /**
20300
+ * Lazy thumbnail URL, never inlined.
20301
+ *
20302
+ * Recording-derived clips (events-mode keep-window, and the prepared
20303
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20304
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20305
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20306
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20307
+ * mint their own stills.
20308
+ *
20309
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20310
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20311
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20312
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20313
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20314
+ * A read that FAILS drops the claim; it never invents it.
20315
+ */
20316
+ thumbnail: string().optional(),
20317
+ /**
20318
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20319
+ * can be decoded. Present whenever the visit came from recorded availability;
20320
+ * absent on a per-event padded window (there is no footage to promise).
20321
+ *
20322
+ * This is not a thumbnail and not a second byte path: it is the argument to
20323
+ * the recorder's existing still route. The surface — never the provider —
20324
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20325
+ * contiguous range, not of the visit: a visit spans its holes by
20326
+ * construction, so a naive midpoint lands in dead air.
20327
+ */
20328
+ stillAtMs: number().optional(),
20329
+ /**
20330
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20331
+ * first within kind (object → motion → audio), capped at
20332
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20333
+ *
20334
+ * Bounded because it is not a payload the surface pages through: one visit on
20335
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20336
+ * camera-day. Read {@link eventCount} for the true total.
20337
+ */
20338
+ eventIds: array(string()).optional(),
20339
+ /** How many analytics events actually overlap this visit. Differs from
20340
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20341
+ * list is never mistaken for a quiet visit. */
20342
+ eventCount: number().int().nonnegative().optional(),
20343
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20344
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20345
+ * bar keeps showing them via `recording.getAvailability`. */
20346
+ holes: array(object({
20347
+ startMs: number(),
20348
+ endMs: number()
20349
+ })).optional()
20243
20350
  });
20244
20351
  var ClipPlaybackSchema = object({
20245
20352
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20703,7 +20810,14 @@ var SearchResultSchema = object({
20703
20810
  });
20704
20811
  var AutoUpdateSettingsSchema = object({
20705
20812
  channel: ChannelSchema,
20706
- intervalSeconds: number()
20813
+ intervalSeconds: number(),
20814
+ /**
20815
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20816
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20817
+ * about a publish and installing it are different decisions. Clamped
20818
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20819
+ */
20820
+ updateCheckIntervalSeconds: number()
20707
20821
  });
20708
20822
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20709
20823
  var RestartAddonResultSchema = unknown();
@@ -20844,7 +20958,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20844
20958
  auth: "admin"
20845
20959
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20846
20960
  channel: ChannelSchema,
20847
- intervalSeconds: number().min(300).max(86400).optional()
20961
+ intervalSeconds: number().min(300).max(86400).optional(),
20962
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20963
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20848
20964
  }), unknown(), {
20849
20965
  kind: "mutation",
20850
20966
  auth: "admin"
@@ -26525,7 +26641,9 @@ var ExportOptionsSchema = object({
26525
26641
  includeAudio: boolean(),
26526
26642
  maxLifeMs: number().int().positive(),
26527
26643
  deleteAfterDownload: boolean(),
26528
- title: string().max(200).optional()
26644
+ title: string().max(200).optional(),
26645
+ /** Notification-output target ids to ping when this export becomes ready. */
26646
+ notifyTargetIds: array(string().min(1)).max(20).optional()
26529
26647
  }).superRefine((v, ctx) => {
26530
26648
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
26531
26649
  code: ZodIssueCode.custom,
@@ -26584,10 +26702,18 @@ var ExportBytesSchema = object({
26584
26702
  });
26585
26703
  method(object({
26586
26704
  deviceId: number(),
26587
- profile: string(),
26705
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
26706
+ profile: string().optional(),
26707
+ profiles: array(string()).min(1).optional(),
26588
26708
  fromMs: number(),
26589
26709
  toMs: number(),
26590
26710
  options: ExportOptionsSchema
26711
+ }).superRefine((v, ctx) => {
26712
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
26713
+ code: ZodIssueCode.custom,
26714
+ message: "pass profiles[] (min 1) or legacy profile",
26715
+ path: ["profiles"]
26716
+ });
26591
26717
  }), ExportRecordSchema, {
26592
26718
  kind: "mutation",
26593
26719
  auth: "protected"
@@ -29529,15 +29655,6 @@ var BaseDeviceProvider = class extends BaseAddon {
29529
29655
  labels: ["probe not implemented"]
29530
29656
  };
29531
29657
  }
29532
- /**
29533
- * Top-level devices restored at once in {@link onRestoreDevices}.
29534
- *
29535
- * Four covers the fleets this ships to without turning a boot into a burst a
29536
- * camera NVR answers with a refusal. A provider whose upstream is a single
29537
- * session with a serial command channel (a Baichuan hub, an NVR that
29538
- * serialises ISAPI) should lower it; nothing needs to raise it.
29539
- */
29540
- restoreConcurrency = 4;
29541
29658
  async restoreDevices(savedDevices) {
29542
29659
  await this.onRestoreDevices(savedDevices);
29543
29660
  if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
@@ -29592,14 +29709,7 @@ var BaseDeviceProvider = class extends BaseAddon {
29592
29709
  });
29593
29710
  }
29594
29711
  };
29595
- let nextTopLevel = 0;
29596
- await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
29597
- for (;;) {
29598
- const saved = topLevel[nextTopLevel++];
29599
- if (saved === void 0) return;
29600
- await restoreOne(saved);
29601
- }
29602
- }));
29712
+ await Promise.all(topLevel.map((saved) => restoreOne(saved)));
29603
29713
  const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
29604
29714
  for (const saved of childRows) {
29605
29715
  const Class = this.deviceClasses[saved.type];
@@ -32872,6 +32982,12 @@ Object.freeze({
32872
32982
  addonId: null,
32873
32983
  access: "create"
32874
32984
  },
32985
+ "pipelineAnalytics.reconcileFromDisk": {
32986
+ capName: "pipeline-analytics",
32987
+ capScope: "device",
32988
+ addonId: null,
32989
+ access: "create"
32990
+ },
32875
32991
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
32876
32992
  capName: "pipeline-analytics",
32877
32993
  capScope: "device",
@@ -33274,6 +33390,12 @@ Object.freeze({
33274
33390
  addonId: null,
33275
33391
  access: "view"
33276
33392
  },
33393
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
33394
+ capName: "pipeline-orchestrator",
33395
+ capScope: "system",
33396
+ addonId: null,
33397
+ access: "view"
33398
+ },
33277
33399
  "pipelineOrchestrator.listAgentSettings": {
33278
33400
  capName: "pipeline-orchestrator",
33279
33401
  capScope: "system",
@@ -33298,6 +33420,12 @@ Object.freeze({
33298
33420
  addonId: null,
33299
33421
  access: "create"
33300
33422
  },
33423
+ "pipelineOrchestrator.reconcileFromDisk": {
33424
+ capName: "pipeline-orchestrator",
33425
+ capScope: "system",
33426
+ addonId: null,
33427
+ access: "create"
33428
+ },
33301
33429
  "pipelineOrchestrator.removeAgentSettings": {
33302
33430
  capName: "pipeline-orchestrator",
33303
33431
  capScope: "system",
@@ -36289,6 +36417,11 @@ Object.freeze({
36289
36417
  form: "single",
36290
36418
  optional: true
36291
36419
  }],
36420
+ "pipelineAnalytics.reconcileFromDisk": [{
36421
+ name: "deviceId",
36422
+ form: "single",
36423
+ optional: false
36424
+ }],
36292
36425
  "pipelineAnalytics.restageRetrainTrack": [{
36293
36426
  name: "deviceId",
36294
36427
  form: "single",
@@ -37406,6 +37539,8 @@ function bestLocationMatch(externalName, existing, threshold = .8) {
37406
37539
  }
37407
37540
  return best;
37408
37541
  }
37542
+ var MB = 1024 * 1024;
37543
+ 1024 * MB, 3072 * MB;
37409
37544
  //#endregion
37410
37545
  //#region src/callback-host.ts
37411
37546
  /**
package/dist/addon.mjs CHANGED
@@ -9,7 +9,7 @@ import { join } from "path";
9
9
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
10
10
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
11
11
  //#endregion
12
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
12
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
13
13
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
14
14
  EventCategory["SystemBoot"] = "system.boot";
15
15
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -26,9 +26,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
26
26
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
27
27
  /**
28
28
  * A newer addon or server-root package version was found by the
29
- * authoritative registry check. Emitted once per
30
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
31
- * polling of the same result is deduplicated by the checker.
29
+ * authoritative registry check. Emitted once when any observed
30
+ * `latestVersion` changes (or a package/node first appears behind);
31
+ * the payload carries the full currently-available list. Repeated
32
+ * polling of the same latests is silent.
32
33
  */
33
34
  EventCategory["UpdateAvailable"] = "update.available";
34
35
  /**
@@ -47,6 +48,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
47
48
  EventCategory["AddonInstalled"] = "addon.installed";
48
49
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
49
50
  EventCategory["AddonCrashed"] = "addon.crashed";
51
+ /**
52
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
53
+ *
54
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
55
+ * respawned"); this is the one that never resolves itself. Emitted exactly
56
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
57
+ * addons it hosted and the crash count that tripped the breaker.
58
+ *
59
+ * It exists because a runner marked terminally `failed` used to be SILENT:
60
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
61
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
62
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
63
+ * operator's phone told him. The Notification Center's system-event intake
64
+ * consumes this category and maps it to the `addon-crash-loop` kind.
65
+ */
66
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
50
67
  EventCategory["AddonError"] = "addon.error";
51
68
  EventCategory["AddonPageReady"] = "addon.page-ready";
52
69
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -7558,6 +7575,10 @@ var RecordingBandSchema = object({
7558
7575
  preBufferSec: number().min(0).optional(),
7559
7576
  postBufferSec: number().min(0).optional()
7560
7577
  });
7578
+ ({
7579
+ preBufferSec: 10,
7580
+ postBufferSec: 30
7581
+ }).postBufferSec * 1e3;
7561
7582
  /**
7562
7583
  * Per-device retention overrides. Every field is optional; an unset or `0`
7563
7584
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7605,6 +7626,12 @@ var RecordingConfigSchema = object({
7605
7626
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
7606
7627
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7607
7628
  mode: RecordingStorageModeSchema.optional(),
7629
+ /**
7630
+ * Which assigned broker slots to record. Absent / empty = {@link
7631
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
7632
+ * camera's currently assigned slots — never `mid` unless the operator
7633
+ * picks it, and never a slot the broker has not assigned.
7634
+ */
7608
7635
  profiles: array(CamProfileSchema).optional(),
7609
7636
  segmentSeconds: number().int().positive().optional(),
7610
7637
  /**
@@ -7685,7 +7712,11 @@ var RelocateFootageInputSchema = object({
7685
7712
  profiles: array(string()).optional(),
7686
7713
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7687
7714
  * never allowed to starve live writers. */
7688
- throttleMbps: number().min(1).max(1e3).optional()
7715
+ throttleMbps: number().min(1).max(1e3).optional(),
7716
+ /** Move only segments whose startMs is >= this. Absent = the whole source
7717
+ * pile. Used when a full drain is too expensive and the operator only
7718
+ * wants the recent window on the new disk. */
7719
+ sinceMs: number().int().optional()
7689
7720
  });
7690
7721
  /** Internal, lease-scoped participant operation. It is intentionally separate
7691
7722
  * from persistent recording settings: a migration never changes
@@ -14616,6 +14647,7 @@ var NcSystemEventKindSchema = _enum([
14616
14647
  "node-offline",
14617
14648
  "node-inference-unavailable",
14618
14649
  "detection-blind",
14650
+ "addon-crash-loop",
14619
14651
  "addon-update-available",
14620
14652
  "server-update-available",
14621
14653
  "alarm-triggered",
@@ -14623,6 +14655,9 @@ var NcSystemEventKindSchema = _enum([
14623
14655
  "alarm-disarmed",
14624
14656
  "alarm-arming",
14625
14657
  "alarm-arm-refused",
14658
+ "addon-updated",
14659
+ "server-updated",
14660
+ "export-completed",
14626
14661
  "camera-online",
14627
14662
  "camera-offline",
14628
14663
  "camera-disabled",
@@ -16516,13 +16551,19 @@ var RetrainStatusSchema = _enum([
16516
16551
  * "never marked" from "already trained" must read `retrainStatus`.
16517
16552
  *
16518
16553
  * `debug` does NOT pin; it is attention, not durability.
16554
+ *
16555
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16556
+ * A favourited track is skipped by retention the same way `staging` is, but
16557
+ * it does not enter `none|staging|trained` and has no staging budget.
16519
16558
  */
16520
16559
  var TrackFlagFields = {
16521
16560
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16522
16561
  * `'staging'`. */
16523
16562
  markForTrain: boolean().optional(),
16524
16563
  /** Operator marked this track for diagnostic attention. */
16525
- debug: boolean().optional()
16564
+ debug: boolean().optional(),
16565
+ /** Operator favourited this track. Pins it against pruning. */
16566
+ favourited: boolean().optional()
16526
16567
  };
16527
16568
  /**
16528
16569
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16547,6 +16588,7 @@ var TrackFlagsSchema = object({
16547
16588
  trackId: string(),
16548
16589
  markForTrain: boolean(),
16549
16590
  debug: boolean(),
16591
+ favourited: boolean(),
16550
16592
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16551
16593
  * a track row) because this shape is only ever produced by the write body,
16552
16594
  * which always knows it — and a surface that has just written needs to render
@@ -17179,6 +17221,12 @@ var TrackCascadeCountsSchema = object({
17179
17221
  /** Per-track CLIP search vectors removed (best-effort). */
17180
17222
  embeddings: number().int()
17181
17223
  });
17224
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17225
+ var DiskReconcileCountsSchema = object({
17226
+ mediaDropped: number().int(),
17227
+ tracks: number().int(),
17228
+ events: number().int()
17229
+ });
17182
17230
  /** Event-store footprint for one camera. */
17183
17231
  var EventStoreDeviceFootprintSchema = object({
17184
17232
  deviceId: number(),
@@ -17355,6 +17403,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17355
17403
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17356
17404
  kind: "mutation",
17357
17405
  auth: "admin"
17406
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17407
+ kind: "mutation",
17408
+ auth: "admin"
17358
17409
  }), method(object({
17359
17410
  deviceId: number(),
17360
17411
  trackIds: array(string()).min(1)
@@ -18905,6 +18956,24 @@ var CameraStatusDegradationSchema = object({
18905
18956
  *
18906
18957
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
18907
18958
  */
18959
+ var DiskReconcileJobSchema = object({
18960
+ state: _enum([
18961
+ "idle",
18962
+ "running",
18963
+ "done",
18964
+ "error"
18965
+ ]),
18966
+ total: number().int().nonnegative(),
18967
+ completed: number().int().nonnegative(),
18968
+ currentDeviceId: number().int().nullable(),
18969
+ failed: array(number().int()).readonly(),
18970
+ mediaDropped: number().int().nonnegative(),
18971
+ tracks: number().int().nonnegative(),
18972
+ events: number().int().nonnegative(),
18973
+ startedAtMs: number().int().nullable(),
18974
+ finishedAtMs: number().int().nullable(),
18975
+ error: string().nullable()
18976
+ });
18908
18977
  var CameraStatusSchema = object({
18909
18978
  deviceId: number(),
18910
18979
  assignment: CameraAssignmentStatusSchema,
@@ -19136,7 +19205,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19136
19205
  }), CameraSwitchGroupSchema, {
19137
19206
  kind: "mutation",
19138
19207
  auth: "admin"
19139
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19208
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
19209
+ kind: "mutation",
19210
+ auth: "admin"
19211
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19140
19212
  name: string(),
19141
19213
  description: string().optional(),
19142
19214
  config: CameraPipelineConfigSchema
@@ -20209,20 +20281,6 @@ var VectorStatsResultSchema = object({
20209
20281
  exact: boolean()
20210
20282
  });
20211
20283
  method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
20212
- /**
20213
- * `videoclips` — the unified, navigable-clip surface for a camera.
20214
- *
20215
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
20216
- * provider per device, substitutable. The DEFAULT provider (registered by
20217
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
20218
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
20219
- * is a time-WINDOW over existing footage, never a separate file. A camera that
20220
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
20221
- * wrapper provider for its device and serve its own clip catalog + URLs.
20222
- *
20223
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
20224
- * temporal overlap, so the API never decides `continuous` vs `events`.
20225
- */
20226
20284
  var ClipSchema = object({
20227
20285
  /** Opaque, provider-namespaced id. The default provider encodes the time
20228
20286
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -20239,8 +20297,57 @@ var ClipSchema = object({
20239
20297
  startMs: number(),
20240
20298
  endMs: number()
20241
20299
  }),
20242
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
20243
- thumbnail: string().optional()
20300
+ /**
20301
+ * Lazy thumbnail URL, never inlined.
20302
+ *
20303
+ * Recording-derived clips (events-mode keep-window, and the prepared
20304
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20305
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20306
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20307
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20308
+ * mint their own stills.
20309
+ *
20310
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20311
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20312
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20313
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20314
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20315
+ * A read that FAILS drops the claim; it never invents it.
20316
+ */
20317
+ thumbnail: string().optional(),
20318
+ /**
20319
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20320
+ * can be decoded. Present whenever the visit came from recorded availability;
20321
+ * absent on a per-event padded window (there is no footage to promise).
20322
+ *
20323
+ * This is not a thumbnail and not a second byte path: it is the argument to
20324
+ * the recorder's existing still route. The surface — never the provider —
20325
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20326
+ * contiguous range, not of the visit: a visit spans its holes by
20327
+ * construction, so a naive midpoint lands in dead air.
20328
+ */
20329
+ stillAtMs: number().optional(),
20330
+ /**
20331
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20332
+ * first within kind (object → motion → audio), capped at
20333
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20334
+ *
20335
+ * Bounded because it is not a payload the surface pages through: one visit on
20336
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20337
+ * camera-day. Read {@link eventCount} for the true total.
20338
+ */
20339
+ eventIds: array(string()).optional(),
20340
+ /** How many analytics events actually overlap this visit. Differs from
20341
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20342
+ * list is never mistaken for a quiet visit. */
20343
+ eventCount: number().int().nonnegative().optional(),
20344
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20345
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20346
+ * bar keeps showing them via `recording.getAvailability`. */
20347
+ holes: array(object({
20348
+ startMs: number(),
20349
+ endMs: number()
20350
+ })).optional()
20244
20351
  });
20245
20352
  var ClipPlaybackSchema = object({
20246
20353
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20704,7 +20811,14 @@ var SearchResultSchema = object({
20704
20811
  });
20705
20812
  var AutoUpdateSettingsSchema = object({
20706
20813
  channel: ChannelSchema,
20707
- intervalSeconds: number()
20814
+ intervalSeconds: number(),
20815
+ /**
20816
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20817
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20818
+ * about a publish and installing it are different decisions. Clamped
20819
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20820
+ */
20821
+ updateCheckIntervalSeconds: number()
20708
20822
  });
20709
20823
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20710
20824
  var RestartAddonResultSchema = unknown();
@@ -20845,7 +20959,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20845
20959
  auth: "admin"
20846
20960
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20847
20961
  channel: ChannelSchema,
20848
- intervalSeconds: number().min(300).max(86400).optional()
20962
+ intervalSeconds: number().min(300).max(86400).optional(),
20963
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20964
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20849
20965
  }), unknown(), {
20850
20966
  kind: "mutation",
20851
20967
  auth: "admin"
@@ -26526,7 +26642,9 @@ var ExportOptionsSchema = object({
26526
26642
  includeAudio: boolean(),
26527
26643
  maxLifeMs: number().int().positive(),
26528
26644
  deleteAfterDownload: boolean(),
26529
- title: string().max(200).optional()
26645
+ title: string().max(200).optional(),
26646
+ /** Notification-output target ids to ping when this export becomes ready. */
26647
+ notifyTargetIds: array(string().min(1)).max(20).optional()
26530
26648
  }).superRefine((v, ctx) => {
26531
26649
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
26532
26650
  code: ZodIssueCode.custom,
@@ -26585,10 +26703,18 @@ var ExportBytesSchema = object({
26585
26703
  });
26586
26704
  method(object({
26587
26705
  deviceId: number(),
26588
- profile: string(),
26706
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
26707
+ profile: string().optional(),
26708
+ profiles: array(string()).min(1).optional(),
26589
26709
  fromMs: number(),
26590
26710
  toMs: number(),
26591
26711
  options: ExportOptionsSchema
26712
+ }).superRefine((v, ctx) => {
26713
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
26714
+ code: ZodIssueCode.custom,
26715
+ message: "pass profiles[] (min 1) or legacy profile",
26716
+ path: ["profiles"]
26717
+ });
26592
26718
  }), ExportRecordSchema, {
26593
26719
  kind: "mutation",
26594
26720
  auth: "protected"
@@ -29530,15 +29656,6 @@ var BaseDeviceProvider = class extends BaseAddon {
29530
29656
  labels: ["probe not implemented"]
29531
29657
  };
29532
29658
  }
29533
- /**
29534
- * Top-level devices restored at once in {@link onRestoreDevices}.
29535
- *
29536
- * Four covers the fleets this ships to without turning a boot into a burst a
29537
- * camera NVR answers with a refusal. A provider whose upstream is a single
29538
- * session with a serial command channel (a Baichuan hub, an NVR that
29539
- * serialises ISAPI) should lower it; nothing needs to raise it.
29540
- */
29541
- restoreConcurrency = 4;
29542
29659
  async restoreDevices(savedDevices) {
29543
29660
  await this.onRestoreDevices(savedDevices);
29544
29661
  if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
@@ -29593,14 +29710,7 @@ var BaseDeviceProvider = class extends BaseAddon {
29593
29710
  });
29594
29711
  }
29595
29712
  };
29596
- let nextTopLevel = 0;
29597
- await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
29598
- for (;;) {
29599
- const saved = topLevel[nextTopLevel++];
29600
- if (saved === void 0) return;
29601
- await restoreOne(saved);
29602
- }
29603
- }));
29713
+ await Promise.all(topLevel.map((saved) => restoreOne(saved)));
29604
29714
  const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
29605
29715
  for (const saved of childRows) {
29606
29716
  const Class = this.deviceClasses[saved.type];
@@ -32873,6 +32983,12 @@ Object.freeze({
32873
32983
  addonId: null,
32874
32984
  access: "create"
32875
32985
  },
32986
+ "pipelineAnalytics.reconcileFromDisk": {
32987
+ capName: "pipeline-analytics",
32988
+ capScope: "device",
32989
+ addonId: null,
32990
+ access: "create"
32991
+ },
32876
32992
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
32877
32993
  capName: "pipeline-analytics",
32878
32994
  capScope: "device",
@@ -33275,6 +33391,12 @@ Object.freeze({
33275
33391
  addonId: null,
33276
33392
  access: "view"
33277
33393
  },
33394
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
33395
+ capName: "pipeline-orchestrator",
33396
+ capScope: "system",
33397
+ addonId: null,
33398
+ access: "view"
33399
+ },
33278
33400
  "pipelineOrchestrator.listAgentSettings": {
33279
33401
  capName: "pipeline-orchestrator",
33280
33402
  capScope: "system",
@@ -33299,6 +33421,12 @@ Object.freeze({
33299
33421
  addonId: null,
33300
33422
  access: "create"
33301
33423
  },
33424
+ "pipelineOrchestrator.reconcileFromDisk": {
33425
+ capName: "pipeline-orchestrator",
33426
+ capScope: "system",
33427
+ addonId: null,
33428
+ access: "create"
33429
+ },
33302
33430
  "pipelineOrchestrator.removeAgentSettings": {
33303
33431
  capName: "pipeline-orchestrator",
33304
33432
  capScope: "system",
@@ -36290,6 +36418,11 @@ Object.freeze({
36290
36418
  form: "single",
36291
36419
  optional: true
36292
36420
  }],
36421
+ "pipelineAnalytics.reconcileFromDisk": [{
36422
+ name: "deviceId",
36423
+ form: "single",
36424
+ optional: false
36425
+ }],
36293
36426
  "pipelineAnalytics.restageRetrainTrack": [{
36294
36427
  name: "deviceId",
36295
36428
  form: "single",
@@ -37407,6 +37540,8 @@ function bestLocationMatch(externalName, existing, threshold = .8) {
37407
37540
  }
37408
37541
  return best;
37409
37542
  }
37543
+ var MB = 1024 * 1024;
37544
+ 1024 * MB, 3072 * MB;
37410
37545
  //#endregion
37411
37546
  //#region src/callback-host.ts
37412
37547
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homematic",
3
- "version": "1.2.21",
3
+ "version": "1.2.22",
4
4
  "description": "Homematic / HomematicIP (CCU3 / RaspberryMatic) device-provider addon for CamStack — wraps the nodehomematic library",
5
5
  "keywords": [
6
6
  "camstack",