@camstack/addon-provider-homematic 1.2.21 → 1.2.23

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 +187 -44
  2. package/dist/addon.mjs +187 -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,65 @@ 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
+ * Distinct object classes attached to this visit (`person`, `car`, …),
20301
+ * dominant first, capped at {@link MAX_CLIP_LABELS}. The ribbon renders these
20302
+ * instead of the bare kind — "Object" tells the operator nothing a colour bar
20303
+ * did not. Absent (never empty) when the visit attached no classified object
20304
+ * event, so a motion/audio-only visit keeps its kind label.
20305
+ */
20306
+ labels: array(string()).max(3).optional(),
20307
+ /**
20308
+ * Lazy thumbnail URL, never inlined.
20309
+ *
20310
+ * Recording-derived clips (events-mode keep-window, and the prepared
20311
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20312
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20313
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20314
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20315
+ * mint their own stills.
20316
+ *
20317
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20318
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20319
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20320
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20321
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20322
+ * A read that FAILS drops the claim; it never invents it.
20323
+ */
20324
+ thumbnail: string().optional(),
20325
+ /**
20326
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20327
+ * can be decoded. Present whenever the visit came from recorded availability;
20328
+ * absent on a per-event padded window (there is no footage to promise).
20329
+ *
20330
+ * This is not a thumbnail and not a second byte path: it is the argument to
20331
+ * the recorder's existing still route. The surface — never the provider —
20332
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20333
+ * contiguous range, not of the visit: a visit spans its holes by
20334
+ * construction, so a naive midpoint lands in dead air.
20335
+ */
20336
+ stillAtMs: number().optional(),
20337
+ /**
20338
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20339
+ * first within kind (object → motion → audio), capped at
20340
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20341
+ *
20342
+ * Bounded because it is not a payload the surface pages through: one visit on
20343
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20344
+ * camera-day. Read {@link eventCount} for the true total.
20345
+ */
20346
+ eventIds: array(string()).optional(),
20347
+ /** How many analytics events actually overlap this visit. Differs from
20348
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20349
+ * list is never mistaken for a quiet visit. */
20350
+ eventCount: number().int().nonnegative().optional(),
20351
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20352
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20353
+ * bar keeps showing them via `recording.getAvailability`. */
20354
+ holes: array(object({
20355
+ startMs: number(),
20356
+ endMs: number()
20357
+ })).optional()
20243
20358
  });
20244
20359
  var ClipPlaybackSchema = object({
20245
20360
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20703,7 +20818,14 @@ var SearchResultSchema = object({
20703
20818
  });
20704
20819
  var AutoUpdateSettingsSchema = object({
20705
20820
  channel: ChannelSchema,
20706
- intervalSeconds: number()
20821
+ intervalSeconds: number(),
20822
+ /**
20823
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20824
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20825
+ * about a publish and installing it are different decisions. Clamped
20826
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20827
+ */
20828
+ updateCheckIntervalSeconds: number()
20707
20829
  });
20708
20830
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20709
20831
  var RestartAddonResultSchema = unknown();
@@ -20844,7 +20966,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20844
20966
  auth: "admin"
20845
20967
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20846
20968
  channel: ChannelSchema,
20847
- intervalSeconds: number().min(300).max(86400).optional()
20969
+ intervalSeconds: number().min(300).max(86400).optional(),
20970
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20971
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20848
20972
  }), unknown(), {
20849
20973
  kind: "mutation",
20850
20974
  auth: "admin"
@@ -26525,7 +26649,9 @@ var ExportOptionsSchema = object({
26525
26649
  includeAudio: boolean(),
26526
26650
  maxLifeMs: number().int().positive(),
26527
26651
  deleteAfterDownload: boolean(),
26528
- title: string().max(200).optional()
26652
+ title: string().max(200).optional(),
26653
+ /** Notification-output target ids to ping when this export becomes ready. */
26654
+ notifyTargetIds: array(string().min(1)).max(20).optional()
26529
26655
  }).superRefine((v, ctx) => {
26530
26656
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
26531
26657
  code: ZodIssueCode.custom,
@@ -26584,10 +26710,18 @@ var ExportBytesSchema = object({
26584
26710
  });
26585
26711
  method(object({
26586
26712
  deviceId: number(),
26587
- profile: string(),
26713
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
26714
+ profile: string().optional(),
26715
+ profiles: array(string()).min(1).optional(),
26588
26716
  fromMs: number(),
26589
26717
  toMs: number(),
26590
26718
  options: ExportOptionsSchema
26719
+ }).superRefine((v, ctx) => {
26720
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
26721
+ code: ZodIssueCode.custom,
26722
+ message: "pass profiles[] (min 1) or legacy profile",
26723
+ path: ["profiles"]
26724
+ });
26591
26725
  }), ExportRecordSchema, {
26592
26726
  kind: "mutation",
26593
26727
  auth: "protected"
@@ -29529,15 +29663,6 @@ var BaseDeviceProvider = class extends BaseAddon {
29529
29663
  labels: ["probe not implemented"]
29530
29664
  };
29531
29665
  }
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
29666
  async restoreDevices(savedDevices) {
29542
29667
  await this.onRestoreDevices(savedDevices);
29543
29668
  if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
@@ -29592,14 +29717,7 @@ var BaseDeviceProvider = class extends BaseAddon {
29592
29717
  });
29593
29718
  }
29594
29719
  };
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
- }));
29720
+ await Promise.all(topLevel.map((saved) => restoreOne(saved)));
29603
29721
  const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
29604
29722
  for (const saved of childRows) {
29605
29723
  const Class = this.deviceClasses[saved.type];
@@ -32872,6 +32990,12 @@ Object.freeze({
32872
32990
  addonId: null,
32873
32991
  access: "create"
32874
32992
  },
32993
+ "pipelineAnalytics.reconcileFromDisk": {
32994
+ capName: "pipeline-analytics",
32995
+ capScope: "device",
32996
+ addonId: null,
32997
+ access: "create"
32998
+ },
32875
32999
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
32876
33000
  capName: "pipeline-analytics",
32877
33001
  capScope: "device",
@@ -33274,6 +33398,12 @@ Object.freeze({
33274
33398
  addonId: null,
33275
33399
  access: "view"
33276
33400
  },
33401
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
33402
+ capName: "pipeline-orchestrator",
33403
+ capScope: "system",
33404
+ addonId: null,
33405
+ access: "view"
33406
+ },
33277
33407
  "pipelineOrchestrator.listAgentSettings": {
33278
33408
  capName: "pipeline-orchestrator",
33279
33409
  capScope: "system",
@@ -33298,6 +33428,12 @@ Object.freeze({
33298
33428
  addonId: null,
33299
33429
  access: "create"
33300
33430
  },
33431
+ "pipelineOrchestrator.reconcileFromDisk": {
33432
+ capName: "pipeline-orchestrator",
33433
+ capScope: "system",
33434
+ addonId: null,
33435
+ access: "create"
33436
+ },
33301
33437
  "pipelineOrchestrator.removeAgentSettings": {
33302
33438
  capName: "pipeline-orchestrator",
33303
33439
  capScope: "system",
@@ -36289,6 +36425,11 @@ Object.freeze({
36289
36425
  form: "single",
36290
36426
  optional: true
36291
36427
  }],
36428
+ "pipelineAnalytics.reconcileFromDisk": [{
36429
+ name: "deviceId",
36430
+ form: "single",
36431
+ optional: false
36432
+ }],
36292
36433
  "pipelineAnalytics.restageRetrainTrack": [{
36293
36434
  name: "deviceId",
36294
36435
  form: "single",
@@ -37406,6 +37547,8 @@ function bestLocationMatch(externalName, existing, threshold = .8) {
37406
37547
  }
37407
37548
  return best;
37408
37549
  }
37550
+ var MB = 1024 * 1024;
37551
+ 1024 * MB, 3072 * MB;
37409
37552
  //#endregion
37410
37553
  //#region src/callback-host.ts
37411
37554
  /**
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,65 @@ 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
+ * Distinct object classes attached to this visit (`person`, `car`, …),
20302
+ * dominant first, capped at {@link MAX_CLIP_LABELS}. The ribbon renders these
20303
+ * instead of the bare kind — "Object" tells the operator nothing a colour bar
20304
+ * did not. Absent (never empty) when the visit attached no classified object
20305
+ * event, so a motion/audio-only visit keeps its kind label.
20306
+ */
20307
+ labels: array(string()).max(3).optional(),
20308
+ /**
20309
+ * Lazy thumbnail URL, never inlined.
20310
+ *
20311
+ * Recording-derived clips (events-mode keep-window, and the prepared
20312
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20313
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20314
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20315
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20316
+ * mint their own stills.
20317
+ *
20318
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20319
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20320
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20321
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20322
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20323
+ * A read that FAILS drops the claim; it never invents it.
20324
+ */
20325
+ thumbnail: string().optional(),
20326
+ /**
20327
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20328
+ * can be decoded. Present whenever the visit came from recorded availability;
20329
+ * absent on a per-event padded window (there is no footage to promise).
20330
+ *
20331
+ * This is not a thumbnail and not a second byte path: it is the argument to
20332
+ * the recorder's existing still route. The surface — never the provider —
20333
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20334
+ * contiguous range, not of the visit: a visit spans its holes by
20335
+ * construction, so a naive midpoint lands in dead air.
20336
+ */
20337
+ stillAtMs: number().optional(),
20338
+ /**
20339
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20340
+ * first within kind (object → motion → audio), capped at
20341
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20342
+ *
20343
+ * Bounded because it is not a payload the surface pages through: one visit on
20344
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20345
+ * camera-day. Read {@link eventCount} for the true total.
20346
+ */
20347
+ eventIds: array(string()).optional(),
20348
+ /** How many analytics events actually overlap this visit. Differs from
20349
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20350
+ * list is never mistaken for a quiet visit. */
20351
+ eventCount: number().int().nonnegative().optional(),
20352
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20353
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20354
+ * bar keeps showing them via `recording.getAvailability`. */
20355
+ holes: array(object({
20356
+ startMs: number(),
20357
+ endMs: number()
20358
+ })).optional()
20244
20359
  });
20245
20360
  var ClipPlaybackSchema = object({
20246
20361
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20704,7 +20819,14 @@ var SearchResultSchema = object({
20704
20819
  });
20705
20820
  var AutoUpdateSettingsSchema = object({
20706
20821
  channel: ChannelSchema,
20707
- intervalSeconds: number()
20822
+ intervalSeconds: number(),
20823
+ /**
20824
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20825
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20826
+ * about a publish and installing it are different decisions. Clamped
20827
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20828
+ */
20829
+ updateCheckIntervalSeconds: number()
20708
20830
  });
20709
20831
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20710
20832
  var RestartAddonResultSchema = unknown();
@@ -20845,7 +20967,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20845
20967
  auth: "admin"
20846
20968
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20847
20969
  channel: ChannelSchema,
20848
- intervalSeconds: number().min(300).max(86400).optional()
20970
+ intervalSeconds: number().min(300).max(86400).optional(),
20971
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20972
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20849
20973
  }), unknown(), {
20850
20974
  kind: "mutation",
20851
20975
  auth: "admin"
@@ -26526,7 +26650,9 @@ var ExportOptionsSchema = object({
26526
26650
  includeAudio: boolean(),
26527
26651
  maxLifeMs: number().int().positive(),
26528
26652
  deleteAfterDownload: boolean(),
26529
- title: string().max(200).optional()
26653
+ title: string().max(200).optional(),
26654
+ /** Notification-output target ids to ping when this export becomes ready. */
26655
+ notifyTargetIds: array(string().min(1)).max(20).optional()
26530
26656
  }).superRefine((v, ctx) => {
26531
26657
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
26532
26658
  code: ZodIssueCode.custom,
@@ -26585,10 +26711,18 @@ var ExportBytesSchema = object({
26585
26711
  });
26586
26712
  method(object({
26587
26713
  deviceId: number(),
26588
- profile: string(),
26714
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
26715
+ profile: string().optional(),
26716
+ profiles: array(string()).min(1).optional(),
26589
26717
  fromMs: number(),
26590
26718
  toMs: number(),
26591
26719
  options: ExportOptionsSchema
26720
+ }).superRefine((v, ctx) => {
26721
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
26722
+ code: ZodIssueCode.custom,
26723
+ message: "pass profiles[] (min 1) or legacy profile",
26724
+ path: ["profiles"]
26725
+ });
26592
26726
  }), ExportRecordSchema, {
26593
26727
  kind: "mutation",
26594
26728
  auth: "protected"
@@ -29530,15 +29664,6 @@ var BaseDeviceProvider = class extends BaseAddon {
29530
29664
  labels: ["probe not implemented"]
29531
29665
  };
29532
29666
  }
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
29667
  async restoreDevices(savedDevices) {
29543
29668
  await this.onRestoreDevices(savedDevices);
29544
29669
  if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
@@ -29593,14 +29718,7 @@ var BaseDeviceProvider = class extends BaseAddon {
29593
29718
  });
29594
29719
  }
29595
29720
  };
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
- }));
29721
+ await Promise.all(topLevel.map((saved) => restoreOne(saved)));
29604
29722
  const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
29605
29723
  for (const saved of childRows) {
29606
29724
  const Class = this.deviceClasses[saved.type];
@@ -32873,6 +32991,12 @@ Object.freeze({
32873
32991
  addonId: null,
32874
32992
  access: "create"
32875
32993
  },
32994
+ "pipelineAnalytics.reconcileFromDisk": {
32995
+ capName: "pipeline-analytics",
32996
+ capScope: "device",
32997
+ addonId: null,
32998
+ access: "create"
32999
+ },
32876
33000
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
32877
33001
  capName: "pipeline-analytics",
32878
33002
  capScope: "device",
@@ -33275,6 +33399,12 @@ Object.freeze({
33275
33399
  addonId: null,
33276
33400
  access: "view"
33277
33401
  },
33402
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
33403
+ capName: "pipeline-orchestrator",
33404
+ capScope: "system",
33405
+ addonId: null,
33406
+ access: "view"
33407
+ },
33278
33408
  "pipelineOrchestrator.listAgentSettings": {
33279
33409
  capName: "pipeline-orchestrator",
33280
33410
  capScope: "system",
@@ -33299,6 +33429,12 @@ Object.freeze({
33299
33429
  addonId: null,
33300
33430
  access: "create"
33301
33431
  },
33432
+ "pipelineOrchestrator.reconcileFromDisk": {
33433
+ capName: "pipeline-orchestrator",
33434
+ capScope: "system",
33435
+ addonId: null,
33436
+ access: "create"
33437
+ },
33302
33438
  "pipelineOrchestrator.removeAgentSettings": {
33303
33439
  capName: "pipeline-orchestrator",
33304
33440
  capScope: "system",
@@ -36290,6 +36426,11 @@ Object.freeze({
36290
36426
  form: "single",
36291
36427
  optional: true
36292
36428
  }],
36429
+ "pipelineAnalytics.reconcileFromDisk": [{
36430
+ name: "deviceId",
36431
+ form: "single",
36432
+ optional: false
36433
+ }],
36293
36434
  "pipelineAnalytics.restageRetrainTrack": [{
36294
36435
  name: "deviceId",
36295
36436
  form: "single",
@@ -37407,6 +37548,8 @@ function bestLocationMatch(externalName, existing, threshold = .8) {
37407
37548
  }
37408
37549
  return best;
37409
37550
  }
37551
+ var MB = 1024 * 1024;
37552
+ 1024 * MB, 3072 * MB;
37410
37553
  //#endregion
37411
37554
  //#region src/callback-host.ts
37412
37555
  /**
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.23",
4
4
  "description": "Homematic / HomematicIP (CCU3 / RaspberryMatic) device-provider addon for CamStack — wraps the nodehomematic library",
5
5
  "keywords": [
6
6
  "camstack",