@camstack/addon-provider-homematic 1.2.20 → 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 +187 -45
  2. package/dist/addon.mjs +187 -45
  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
@@ -19540,7 +19612,14 @@ targets: array(object({
19540
19612
  "sleeping",
19541
19613
  "unreachable",
19542
19614
  "waking"
19543
- ]).nullable()
19615
+ ]).nullable(),
19616
+ /** A battery camera (whatever its current state). An AWAKE battery
19617
+ * camera is deliberately NOT recaptured on the poll cadence — every
19618
+ * capture is a camera hit that would keep it out of sleep — so its
19619
+ * cached frame legitimately ages past the currency ceiling while
19620
+ * nothing is streaming. A surface must keep painting it (a fresh
19621
+ * frame is captured at each wake), not blank to "unavailable". */
19622
+ battery: boolean()
19544
19623
  })));
19545
19624
  /**
19546
19625
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
@@ -20201,20 +20280,6 @@ var VectorStatsResultSchema = object({
20201
20280
  exact: boolean()
20202
20281
  });
20203
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);
20204
- /**
20205
- * `videoclips` — the unified, navigable-clip surface for a camera.
20206
- *
20207
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
20208
- * provider per device, substitutable. The DEFAULT provider (registered by
20209
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
20210
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
20211
- * is a time-WINDOW over existing footage, never a separate file. A camera that
20212
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
20213
- * wrapper provider for its device and serve its own clip catalog + URLs.
20214
- *
20215
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
20216
- * temporal overlap, so the API never decides `continuous` vs `events`.
20217
- */
20218
20283
  var ClipSchema = object({
20219
20284
  /** Opaque, provider-namespaced id. The default provider encodes the time
20220
20285
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -20231,8 +20296,57 @@ var ClipSchema = object({
20231
20296
  startMs: number(),
20232
20297
  endMs: number()
20233
20298
  }),
20234
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
20235
- 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()
20236
20350
  });
20237
20351
  var ClipPlaybackSchema = object({
20238
20352
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20696,7 +20810,14 @@ var SearchResultSchema = object({
20696
20810
  });
20697
20811
  var AutoUpdateSettingsSchema = object({
20698
20812
  channel: ChannelSchema,
20699
- 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()
20700
20821
  });
20701
20822
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20702
20823
  var RestartAddonResultSchema = unknown();
@@ -20837,7 +20958,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20837
20958
  auth: "admin"
20838
20959
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20839
20960
  channel: ChannelSchema,
20840
- 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()
20841
20964
  }), unknown(), {
20842
20965
  kind: "mutation",
20843
20966
  auth: "admin"
@@ -26518,7 +26641,9 @@ var ExportOptionsSchema = object({
26518
26641
  includeAudio: boolean(),
26519
26642
  maxLifeMs: number().int().positive(),
26520
26643
  deleteAfterDownload: boolean(),
26521
- 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()
26522
26647
  }).superRefine((v, ctx) => {
26523
26648
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
26524
26649
  code: ZodIssueCode.custom,
@@ -26577,10 +26702,18 @@ var ExportBytesSchema = object({
26577
26702
  });
26578
26703
  method(object({
26579
26704
  deviceId: number(),
26580
- profile: string(),
26705
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
26706
+ profile: string().optional(),
26707
+ profiles: array(string()).min(1).optional(),
26581
26708
  fromMs: number(),
26582
26709
  toMs: number(),
26583
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
+ });
26584
26717
  }), ExportRecordSchema, {
26585
26718
  kind: "mutation",
26586
26719
  auth: "protected"
@@ -29522,15 +29655,6 @@ var BaseDeviceProvider = class extends BaseAddon {
29522
29655
  labels: ["probe not implemented"]
29523
29656
  };
29524
29657
  }
29525
- /**
29526
- * Top-level devices restored at once in {@link onRestoreDevices}.
29527
- *
29528
- * Four covers the fleets this ships to without turning a boot into a burst a
29529
- * camera NVR answers with a refusal. A provider whose upstream is a single
29530
- * session with a serial command channel (a Baichuan hub, an NVR that
29531
- * serialises ISAPI) should lower it; nothing needs to raise it.
29532
- */
29533
- restoreConcurrency = 4;
29534
29658
  async restoreDevices(savedDevices) {
29535
29659
  await this.onRestoreDevices(savedDevices);
29536
29660
  if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
@@ -29585,14 +29709,7 @@ var BaseDeviceProvider = class extends BaseAddon {
29585
29709
  });
29586
29710
  }
29587
29711
  };
29588
- let nextTopLevel = 0;
29589
- await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
29590
- for (;;) {
29591
- const saved = topLevel[nextTopLevel++];
29592
- if (saved === void 0) return;
29593
- await restoreOne(saved);
29594
- }
29595
- }));
29712
+ await Promise.all(topLevel.map((saved) => restoreOne(saved)));
29596
29713
  const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
29597
29714
  for (const saved of childRows) {
29598
29715
  const Class = this.deviceClasses[saved.type];
@@ -32865,6 +32982,12 @@ Object.freeze({
32865
32982
  addonId: null,
32866
32983
  access: "create"
32867
32984
  },
32985
+ "pipelineAnalytics.reconcileFromDisk": {
32986
+ capName: "pipeline-analytics",
32987
+ capScope: "device",
32988
+ addonId: null,
32989
+ access: "create"
32990
+ },
32868
32991
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
32869
32992
  capName: "pipeline-analytics",
32870
32993
  capScope: "device",
@@ -33267,6 +33390,12 @@ Object.freeze({
33267
33390
  addonId: null,
33268
33391
  access: "view"
33269
33392
  },
33393
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
33394
+ capName: "pipeline-orchestrator",
33395
+ capScope: "system",
33396
+ addonId: null,
33397
+ access: "view"
33398
+ },
33270
33399
  "pipelineOrchestrator.listAgentSettings": {
33271
33400
  capName: "pipeline-orchestrator",
33272
33401
  capScope: "system",
@@ -33291,6 +33420,12 @@ Object.freeze({
33291
33420
  addonId: null,
33292
33421
  access: "create"
33293
33422
  },
33423
+ "pipelineOrchestrator.reconcileFromDisk": {
33424
+ capName: "pipeline-orchestrator",
33425
+ capScope: "system",
33426
+ addonId: null,
33427
+ access: "create"
33428
+ },
33294
33429
  "pipelineOrchestrator.removeAgentSettings": {
33295
33430
  capName: "pipeline-orchestrator",
33296
33431
  capScope: "system",
@@ -36282,6 +36417,11 @@ Object.freeze({
36282
36417
  form: "single",
36283
36418
  optional: true
36284
36419
  }],
36420
+ "pipelineAnalytics.reconcileFromDisk": [{
36421
+ name: "deviceId",
36422
+ form: "single",
36423
+ optional: false
36424
+ }],
36285
36425
  "pipelineAnalytics.restageRetrainTrack": [{
36286
36426
  name: "deviceId",
36287
36427
  form: "single",
@@ -37399,6 +37539,8 @@ function bestLocationMatch(externalName, existing, threshold = .8) {
37399
37539
  }
37400
37540
  return best;
37401
37541
  }
37542
+ var MB = 1024 * 1024;
37543
+ 1024 * MB, 3072 * MB;
37402
37544
  //#endregion
37403
37545
  //#region src/callback-host.ts
37404
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
@@ -19541,7 +19613,14 @@ targets: array(object({
19541
19613
  "sleeping",
19542
19614
  "unreachable",
19543
19615
  "waking"
19544
- ]).nullable()
19616
+ ]).nullable(),
19617
+ /** A battery camera (whatever its current state). An AWAKE battery
19618
+ * camera is deliberately NOT recaptured on the poll cadence — every
19619
+ * capture is a camera hit that would keep it out of sleep — so its
19620
+ * cached frame legitimately ages past the currency ceiling while
19621
+ * nothing is streaming. A surface must keep painting it (a fresh
19622
+ * frame is captured at each wake), not blank to "unavailable". */
19623
+ battery: boolean()
19545
19624
  })));
19546
19625
  /**
19547
19626
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
@@ -20202,20 +20281,6 @@ var VectorStatsResultSchema = object({
20202
20281
  exact: boolean()
20203
20282
  });
20204
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);
20205
- /**
20206
- * `videoclips` — the unified, navigable-clip surface for a camera.
20207
- *
20208
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
20209
- * provider per device, substitutable. The DEFAULT provider (registered by
20210
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
20211
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
20212
- * is a time-WINDOW over existing footage, never a separate file. A camera that
20213
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
20214
- * wrapper provider for its device and serve its own clip catalog + URLs.
20215
- *
20216
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
20217
- * temporal overlap, so the API never decides `continuous` vs `events`.
20218
- */
20219
20284
  var ClipSchema = object({
20220
20285
  /** Opaque, provider-namespaced id. The default provider encodes the time
20221
20286
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -20232,8 +20297,57 @@ var ClipSchema = object({
20232
20297
  startMs: number(),
20233
20298
  endMs: number()
20234
20299
  }),
20235
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
20236
- 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()
20237
20351
  });
20238
20352
  var ClipPlaybackSchema = object({
20239
20353
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20697,7 +20811,14 @@ var SearchResultSchema = object({
20697
20811
  });
20698
20812
  var AutoUpdateSettingsSchema = object({
20699
20813
  channel: ChannelSchema,
20700
- 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()
20701
20822
  });
20702
20823
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20703
20824
  var RestartAddonResultSchema = unknown();
@@ -20838,7 +20959,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20838
20959
  auth: "admin"
20839
20960
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20840
20961
  channel: ChannelSchema,
20841
- 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()
20842
20965
  }), unknown(), {
20843
20966
  kind: "mutation",
20844
20967
  auth: "admin"
@@ -26519,7 +26642,9 @@ var ExportOptionsSchema = object({
26519
26642
  includeAudio: boolean(),
26520
26643
  maxLifeMs: number().int().positive(),
26521
26644
  deleteAfterDownload: boolean(),
26522
- 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()
26523
26648
  }).superRefine((v, ctx) => {
26524
26649
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
26525
26650
  code: ZodIssueCode.custom,
@@ -26578,10 +26703,18 @@ var ExportBytesSchema = object({
26578
26703
  });
26579
26704
  method(object({
26580
26705
  deviceId: number(),
26581
- profile: string(),
26706
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
26707
+ profile: string().optional(),
26708
+ profiles: array(string()).min(1).optional(),
26582
26709
  fromMs: number(),
26583
26710
  toMs: number(),
26584
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
+ });
26585
26718
  }), ExportRecordSchema, {
26586
26719
  kind: "mutation",
26587
26720
  auth: "protected"
@@ -29523,15 +29656,6 @@ var BaseDeviceProvider = class extends BaseAddon {
29523
29656
  labels: ["probe not implemented"]
29524
29657
  };
29525
29658
  }
29526
- /**
29527
- * Top-level devices restored at once in {@link onRestoreDevices}.
29528
- *
29529
- * Four covers the fleets this ships to without turning a boot into a burst a
29530
- * camera NVR answers with a refusal. A provider whose upstream is a single
29531
- * session with a serial command channel (a Baichuan hub, an NVR that
29532
- * serialises ISAPI) should lower it; nothing needs to raise it.
29533
- */
29534
- restoreConcurrency = 4;
29535
29659
  async restoreDevices(savedDevices) {
29536
29660
  await this.onRestoreDevices(savedDevices);
29537
29661
  if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
@@ -29586,14 +29710,7 @@ var BaseDeviceProvider = class extends BaseAddon {
29586
29710
  });
29587
29711
  }
29588
29712
  };
29589
- let nextTopLevel = 0;
29590
- await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
29591
- for (;;) {
29592
- const saved = topLevel[nextTopLevel++];
29593
- if (saved === void 0) return;
29594
- await restoreOne(saved);
29595
- }
29596
- }));
29713
+ await Promise.all(topLevel.map((saved) => restoreOne(saved)));
29597
29714
  const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
29598
29715
  for (const saved of childRows) {
29599
29716
  const Class = this.deviceClasses[saved.type];
@@ -32866,6 +32983,12 @@ Object.freeze({
32866
32983
  addonId: null,
32867
32984
  access: "create"
32868
32985
  },
32986
+ "pipelineAnalytics.reconcileFromDisk": {
32987
+ capName: "pipeline-analytics",
32988
+ capScope: "device",
32989
+ addonId: null,
32990
+ access: "create"
32991
+ },
32869
32992
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
32870
32993
  capName: "pipeline-analytics",
32871
32994
  capScope: "device",
@@ -33268,6 +33391,12 @@ Object.freeze({
33268
33391
  addonId: null,
33269
33392
  access: "view"
33270
33393
  },
33394
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
33395
+ capName: "pipeline-orchestrator",
33396
+ capScope: "system",
33397
+ addonId: null,
33398
+ access: "view"
33399
+ },
33271
33400
  "pipelineOrchestrator.listAgentSettings": {
33272
33401
  capName: "pipeline-orchestrator",
33273
33402
  capScope: "system",
@@ -33292,6 +33421,12 @@ Object.freeze({
33292
33421
  addonId: null,
33293
33422
  access: "create"
33294
33423
  },
33424
+ "pipelineOrchestrator.reconcileFromDisk": {
33425
+ capName: "pipeline-orchestrator",
33426
+ capScope: "system",
33427
+ addonId: null,
33428
+ access: "create"
33429
+ },
33295
33430
  "pipelineOrchestrator.removeAgentSettings": {
33296
33431
  capName: "pipeline-orchestrator",
33297
33432
  capScope: "system",
@@ -36283,6 +36418,11 @@ Object.freeze({
36283
36418
  form: "single",
36284
36419
  optional: true
36285
36420
  }],
36421
+ "pipelineAnalytics.reconcileFromDisk": [{
36422
+ name: "deviceId",
36423
+ form: "single",
36424
+ optional: false
36425
+ }],
36286
36426
  "pipelineAnalytics.restageRetrainTrack": [{
36287
36427
  name: "deviceId",
36288
36428
  form: "single",
@@ -37400,6 +37540,8 @@ function bestLocationMatch(externalName, existing, threshold = .8) {
37400
37540
  }
37401
37541
  return best;
37402
37542
  }
37543
+ var MB = 1024 * 1024;
37544
+ 1024 * MB, 3072 * MB;
37403
37545
  //#endregion
37404
37546
  //#region src/callback-host.ts
37405
37547
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homematic",
3
- "version": "1.2.20",
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",