@camstack/addon-provider-reolink 1.2.182 → 1.2.184

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 +859 -283
  2. package/dist/addon.mjs +858 -282
  3. package/package.json +1 -1
package/dist/addon.mjs CHANGED
@@ -10,6 +10,7 @@ import * as crypto$7 from "crypto";
10
10
  import crypto$1, { createHash as createHash$1, randomUUID } from "crypto";
11
11
  import { execFile, spawn } from "child_process";
12
12
  import { createServer } from "node:http";
13
+ import "node:zlib";
13
14
  import { mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
14
15
  import { join as join$1 } from "node:path";
15
16
  import { PassThrough } from "stream";
@@ -27,7 +28,7 @@ import { URL as URL$1 } from "url";
27
28
  import { spawn as spawn$1 } from "node:child_process";
28
29
  import os, { tmpdir } from "node:os";
29
30
  import { createReadStream } from "node:fs";
30
- //#region ../types/dist/event-category-DaIZi00R.mjs
31
+ //#region ../types/dist/event-category-C4hVnRo8.mjs
31
32
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
32
33
  EventCategory["SystemBoot"] = "system.boot";
33
34
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -246,7 +247,17 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
246
247
  EventCategory["RecordingStopped"] = "recording.stopped";
247
248
  EventCategory["RecordingError"] = "recording.error";
248
249
  EventCategory["RecordingHealthDegraded"] = "recording.health.degraded";
250
+ /** Recording enabled but no segment finalized for the NC stall window. */
251
+ EventCategory["RecordingHealthStalled"] = "recording.health.stalled";
252
+ /** Recovery from {@link RecordingHealthStalled}. */
253
+ EventCategory["RecordingHealthOk"] = "recording.health.ok";
249
254
  EventCategory["RecordingStorageCritical"] = "recording.storage.critical";
255
+ /** Free space on a storage location crossed the warn bar. */
256
+ EventCategory["StorageLocationLow"] = "storage.location.low";
257
+ /** Free space at/under the critical bar, or capacity probe failed (`null`). */
258
+ EventCategory["StorageLocationCritical"] = "storage.location.critical";
259
+ /** Recovery from low/critical on a storage location. */
260
+ EventCategory["StorageLocationOk"] = "storage.location.ok";
250
261
  EventCategory["RecordingSegmentWritten"] = "recording.segment.written";
251
262
  EventCategory["RecordingPolicyFallback"] = "recording.policy.fallback";
252
263
  EventCategory["RecordingRetentionCompleted"] = "recording.retention.completed";
@@ -5405,7 +5416,7 @@ var ZodIssueCode = {
5405
5416
  var ZodFirstPartyTypeKind;
5406
5417
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5407
5418
  //#endregion
5408
- //#region ../types/dist/sleep-r3RWkGVx.mjs
5419
+ //#region ../types/dist/sleep-vWTnHq9G.mjs
5409
5420
  /**
5410
5421
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5411
5422
  * window to float samples (D455).
@@ -8010,6 +8021,21 @@ var ProfileRtspEntrySchema = object({
8010
8021
  codec: string().optional(),
8011
8022
  resolution: CamStreamResolutionSchema.optional()
8012
8023
  });
8024
+ /**
8025
+ * The model weight formats a catalog entry can ship. Schema-free on purpose:
8026
+ * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
8027
+ * (the model downloader in `@camstack/system`) needs only the list and must
8028
+ * reach it through `@camstack/types/addon` without building the cap schema
8029
+ * graph (D28).
8030
+ */
8031
+ var MODEL_FORMATS = [
8032
+ "onnx",
8033
+ "coreml",
8034
+ "openvino",
8035
+ "tflite",
8036
+ "pt",
8037
+ "gguf"
8038
+ ];
8013
8039
  object({
8014
8040
  field: string().min(1),
8015
8041
  on: literal("advance")
@@ -8608,7 +8634,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
8608
8634
  return DeviceRole;
8609
8635
  }({});
8610
8636
  //#endregion
8611
- //#region ../types/dist/composition-Bzr7GwgP.mjs
8637
+ //#region ../types/dist/composition-C-WpVYTX.mjs
8612
8638
  /**
8613
8639
  * The ffmpeg build every CamStack node downloads, per platform and arch.
8614
8640
  *
@@ -8660,7 +8686,7 @@ var BASE = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFM
8660
8686
  function portable(target) {
8661
8687
  return `${BASE}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
8662
8688
  }
8663
- portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64");
8689
+ portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64"), `${BASE}${FFMPEG_RELEASE}`;
8664
8690
  /**
8665
8691
  * THE table "which boolean of this device type counts as HIGH", and the
8666
8692
  * pure evaluator of its EDGE.
@@ -9463,6 +9489,16 @@ var CompositionSchema = object({
9463
9489
  seen.add(feature.cap);
9464
9490
  });
9465
9491
  });
9492
+ var NodeColorIdSchema = _enum([
9493
+ "node-a",
9494
+ "node-b",
9495
+ "node-c",
9496
+ "node-d",
9497
+ "node-e",
9498
+ "node-f",
9499
+ "node-g",
9500
+ "node-h"
9501
+ ]);
9466
9502
  new Set([
9467
9503
  "track",
9468
9504
  "face",
@@ -12394,14 +12430,6 @@ var MASK_SUPPLY_STEP_IDS = ["segmentation"];
12394
12430
  ...CROP_SUPPLY_STEP_IDS,
12395
12431
  ...MASK_SUPPLY_STEP_IDS
12396
12432
  ];
12397
- var MODEL_FORMATS = [
12398
- "onnx",
12399
- "coreml",
12400
- "openvino",
12401
- "tflite",
12402
- "pt",
12403
- "gguf"
12404
- ];
12405
12433
  /**
12406
12434
  * Multi-file format payload.
12407
12435
  *
@@ -14225,7 +14253,14 @@ var BrokerProviderInfoSchema = object({
14225
14253
  /** Broker kinds this provider can create, with a display label. */
14226
14254
  kinds: array(object({
14227
14255
  kind: string(),
14228
- label: string()
14256
+ label: string(),
14257
+ /**
14258
+ * Relative hub path to start an interactive account link (OAuth popup).
14259
+ * May include `{{brokerId}}`; admin-ui substitutes and appends `?popup=1`.
14260
+ */
14261
+ interactiveConnectPath: string().min(1).optional(),
14262
+ /** Relative hub path to disconnect the linked account (same popup pattern). */
14263
+ interactiveDisconnectPath: string().min(1).optional()
14229
14264
  }))
14230
14265
  });
14231
14266
  var ListInputSchema = object({
@@ -19176,14 +19211,14 @@ var LlmProfileSchema = object({
19176
19211
  /** ConfigUISchema tree passed through untyped on the wire (the
19177
19212
  * notification-output `ConfigSchemaPassthrough` precedent at
19178
19213
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
19179
- var ConfigSchemaPassthrough$1 = unknown();
19214
+ var ConfigSchemaPassthrough$3 = unknown();
19180
19215
  var LlmProfileKindDescriptorSchema = object({
19181
19216
  kind: LlmProfileKindSchema,
19182
19217
  label: string(),
19183
19218
  icon: string(),
19184
19219
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
19185
19220
  addonId: string(),
19186
- configSchema: ConfigSchemaPassthrough$1
19221
+ configSchema: ConfigSchemaPassthrough$3
19187
19222
  });
19188
19223
  var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
19189
19224
  var LlmDefaultSchema = object({
@@ -20325,7 +20360,7 @@ var TargetKindCapsSchema = object({
20325
20360
  * the union is large and not meant for runtime validation here; the exported
20326
20361
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
20327
20362
  */
20328
- var ConfigSchemaPassthrough = unknown();
20363
+ var ConfigSchemaPassthrough$2 = unknown();
20329
20364
  var TargetKindSchema = object({
20330
20365
  kind: string(),
20331
20366
  label: string(),
@@ -20362,7 +20397,7 @@ var TargetKindSchema = object({
20362
20397
  * its raster path, which is the safe default for an unknown image.
20363
20398
  */
20364
20399
  iconMediaType: string().optional(),
20365
- configSchema: ConfigSchemaPassthrough,
20400
+ configSchema: ConfigSchemaPassthrough$2,
20366
20401
  /**
20367
20402
  * The config keys that decide WHERE this kind's secrets are sent (server,
20368
20403
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -21319,6 +21354,12 @@ var NcSystemEventKindSchema = _enum([
21319
21354
  "node-updating",
21320
21355
  "node-inference-unavailable",
21321
21356
  "detection-blind",
21357
+ "recording-stalled",
21358
+ "recording-healthy",
21359
+ "recording-degraded",
21360
+ "storage-location-low",
21361
+ "storage-location-critical",
21362
+ "storage-location-ok",
21322
21363
  "addon-crash-loop",
21323
21364
  "addon-update-available",
21324
21365
  "server-update-available",
@@ -23699,6 +23740,61 @@ var OauthIntegrationDescriptorSchema = object({
23699
23740
  });
23700
23741
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
23701
23742
  /**
23743
+ * `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
23744
+ *
23745
+ * One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
23746
+ * ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
23747
+ * Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
23748
+ * immutable after create.
23749
+ */
23750
+ var ConfigSchemaPassthrough$1 = unknown();
23751
+ var OidcConnectionSchema = object({
23752
+ id: string().min(1),
23753
+ displayName: string(),
23754
+ icon: string(),
23755
+ issuerUrl: string(),
23756
+ clientId: string(),
23757
+ /** Client id + secret present — never the secret value. */
23758
+ configured: boolean(),
23759
+ /**
23760
+ * Values for the connection form schema. `clientSecret` is a keychain
23761
+ * {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
23762
+ */
23763
+ formValues: record(string(), unknown()).optional()
23764
+ });
23765
+ /**
23766
+ * Patch one connection. Omitted or empty `clientSecret` keeps the stored
23767
+ * secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
23768
+ */
23769
+ var OidcConnectionUpdateInputSchema = object({
23770
+ id: string().min(1),
23771
+ displayName: string().min(1),
23772
+ icon: string().optional(),
23773
+ issuerUrl: string().optional(),
23774
+ clientId: string().optional(),
23775
+ clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
23776
+ redirectUri: string().optional(),
23777
+ scopes: string().optional(),
23778
+ usernameClaim: _enum([
23779
+ "preferred_username",
23780
+ "email",
23781
+ "sub"
23782
+ ]).optional()
23783
+ });
23784
+ method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
23785
+ id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
23786
+ displayName: string().min(1)
23787
+ }), object({ id: string() }), {
23788
+ kind: "mutation",
23789
+ auth: "admin"
23790
+ }), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
23791
+ kind: "mutation",
23792
+ auth: "admin"
23793
+ }), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
23794
+ kind: "mutation",
23795
+ auth: "admin"
23796
+ });
23797
+ /**
23702
23798
  * What started a camera's onboard motion (D672). `camera` is the camera's own
23703
23799
  * firmware; `sensor` is a device the operator ATTACHED to it. Absent on the
23704
23800
  * wire means the camera: only the camera-sensors bridge ever says `sensor`.
@@ -25055,24 +25151,69 @@ var RetrainFrameListSchema = object({
25055
25151
  /** What the page pre-selects — the native key frame when one survives. */
25056
25152
  autoPickMediaId: number().int().optional()
25057
25153
  });
25154
+ /** Exclusive partition of a corpus copy's annotation progress (design §4.2). */
25155
+ var RetrainFrameWorkStateSchema = _enum([
25156
+ "unannotated",
25157
+ "annotated",
25158
+ "complete"
25159
+ ]);
25160
+ /**
25161
+ * Filters for paginated corpus list, KPI, and matching export (design §4.4).
25162
+ * Every narrowing field is ANDed; omitted fields impose no constraint.
25163
+ */
25164
+ var RetrainCorpusFilterSchema = object({
25165
+ workState: RetrainFrameWorkStateSchema.optional(),
25166
+ deviceIds: array(number()).max(500).optional(),
25167
+ /** When set, only frames in this named collection. */
25168
+ collectionId: string().optional(),
25169
+ /** Frames that carry at least one saved annotation in one of these classes. */
25170
+ classes: array(RetrainMacroClassSchema).optional(),
25171
+ page: number().int().positive().default(1),
25172
+ pageSize: number().int().positive().max(100).default(50)
25173
+ });
25174
+ var RetrainCorpusPageSchema = object({
25175
+ frames: array(RetrainFrameSchema.extend({ workState: RetrainFrameWorkStateSchema })).readonly(),
25176
+ total: number().int().nonnegative(),
25177
+ page: number().int().positive(),
25178
+ pageSize: number().int().positive()
25179
+ });
25180
+ var RetrainCorpusKpiCameraRowSchema = object({
25181
+ deviceId: number(),
25182
+ unannotated: number().int().nonnegative(),
25183
+ annotated: number().int().nonnegative(),
25184
+ complete: number().int().nonnegative(),
25185
+ /** This camera's `complete` count divided by corpus `total`, in `[0,1]`.
25186
+ * `null` when the corpus is empty (no ratio). */
25187
+ pctOfCorpus: number().min(0).max(1).nullable()
25188
+ });
25189
+ var RetrainCorpusKpiSchema = object({
25190
+ total: number().int().nonnegative(),
25191
+ unannotated: number().int().nonnegative(),
25192
+ annotated: number().int().nonnegative(),
25193
+ complete: number().int().nonnegative(),
25194
+ cameras: array(RetrainCorpusKpiCameraRowSchema).readonly()
25195
+ });
25058
25196
  /**
25059
25197
  * Which frames an annotated export carries. Every field narrows; an empty
25060
- * filter is every annotated copy. A dataset VERSION is a frozen `frameIds`
25061
- * list owned by whoever trains (the training orchestrator), so `frameIds` is
25062
- * the primitive a version export uses; `batchId` re-selects what an earlier
25063
- * export carried (`exportedInBatch`).
25198
+ * filter is every annotated copy. `frameIds` is the primitive a job or
25199
+ * download uses; `batchId` re-selects what an earlier export carried
25200
+ * (`exportedInBatch`). Frozen dataset versions were removed — live selection
25201
+ * resolves to `frameIds` at plan/start.
25064
25202
  */
25065
25203
  var RetrainExportFilterSchema = object({
25066
25204
  deviceIds: array(number()).optional(),
25067
25205
  frameIds: array(string()).max(2e4).optional(),
25068
25206
  /** Only frames in at least one of these named collections. */
25069
25207
  collectionIds: array(string()).max(200).optional(),
25070
- /** Exactly the frames of this dataset version (and its completeOnly). An
25071
- * unknown id selects nothing. */
25072
- datasetVersionId: string().optional(),
25208
+ /** Single collection (Frames filter bar). ANDed with `collectionIds` when both set. */
25209
+ collectionId: string().optional(),
25073
25210
  batchId: string().optional(),
25074
25211
  /** Only frames the operator marked complete (all subjects boxed). */
25075
- completeOnly: boolean().optional()
25212
+ completeOnly: boolean().optional(),
25213
+ /** Exclusive work-state partition (`workStateOf`); same as the corpus list. */
25214
+ workState: RetrainFrameWorkStateSchema.optional(),
25215
+ /** Frames with at least one saved annotation in one of these classes. */
25216
+ classes: array(RetrainMacroClassSchema).optional()
25076
25217
  });
25077
25218
  /**
25078
25219
  * `frames`: the full frame copies, boxes in `annotations.json`.
@@ -25272,8 +25413,7 @@ var RetrainDatasetStatsSchema = object({
25272
25413
  completeFrames: number().int()
25273
25414
  })).readonly()
25274
25415
  });
25275
- /** An immutable dataset version, without its frame list. */
25276
- var RetrainDatasetVersionSchema = object({
25416
+ object({
25277
25417
  id: string(),
25278
25418
  name: string(),
25279
25419
  createdAt: number(),
@@ -25281,22 +25421,33 @@ var RetrainDatasetVersionSchema = object({
25281
25421
  layout: RetrainExportLayoutSchema,
25282
25422
  completeOnly: boolean(),
25283
25423
  frameCount: number().int(),
25284
- /** sha256 of what the frames taught when the version was frozen. */
25285
25424
  annotationHash: string(),
25286
- /** The export manifest schema the version was cut under. */
25287
25425
  manifestSchema: string(),
25288
25426
  stats: RetrainDatasetStatsSchema
25289
25427
  });
25290
- var CreateRetrainDatasetVersionResultSchema = discriminatedUnion("kind", [object({
25291
- kind: literal("created"),
25292
- version: RetrainDatasetVersionSchema
25428
+ /**
25429
+ * Live training corpus criteria — same shape as `TrainingCorpusSelection` on
25430
+ * the training cap. Resolved here so plan/start and the annotated export share
25431
+ * one definition of "the frames" (`buildRetrainExportPlan`).
25432
+ */
25433
+ var RetrainTrainingCorpusSelectionSchema = object({
25434
+ collectionIds: array(string()).min(1),
25435
+ workState: _enum([
25436
+ "complete",
25437
+ "annotated_or_complete",
25438
+ "any_annotated"
25439
+ ]).default("complete")
25440
+ });
25441
+ var RetrainTrainingCorpusResolveResultSchema = discriminatedUnion("kind", [object({
25442
+ kind: literal("resolved"),
25443
+ frameIds: array(string()).max(2e4).readonly(),
25444
+ stats: RetrainDatasetStatsSchema,
25445
+ annotationHash: string(),
25446
+ layout: RetrainExportLayoutSchema
25293
25447
  }), object({
25294
25448
  kind: literal("refused"),
25295
- reason: _enum([
25296
- "empty-name",
25297
- "duplicate-name",
25298
- "no-frames"
25299
- ])
25449
+ reason: _enum(["unknown-collection", "no-frames"]),
25450
+ detail: string()
25300
25451
  })]);
25301
25452
  /** The outcome of a lifecycle move owned by the retrain page. */
25302
25453
  var RetrainTransitionResultSchema = object({
@@ -26003,6 +26154,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
26003
26154
  }), array(RetrainTrackSchema).readonly(), {
26004
26155
  kind: "query",
26005
26156
  auth: "admin"
26157
+ }), method(RetrainCorpusFilterSchema, RetrainCorpusPageSchema, {
26158
+ kind: "query",
26159
+ auth: "admin"
26160
+ }), method(object({}), RetrainCorpusKpiSchema, {
26161
+ kind: "query",
26162
+ auth: "admin"
26006
26163
  }), method(object({ trackId: string() }), RetrainFrameListSchema, {
26007
26164
  kind: "query",
26008
26165
  auth: "admin"
@@ -26098,36 +26255,6 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
26098
26255
  kind: "mutation",
26099
26256
  auth: "admin",
26100
26257
  timeoutMs: 12e4
26101
- }), method(object({
26102
- selection: RetrainDatasetSelectionSchema,
26103
- completeOnly: boolean().optional()
26104
- }), RetrainDatasetStatsSchema, {
26105
- kind: "query",
26106
- auth: "admin"
26107
- }), method(object({
26108
- name: string().max(120),
26109
- selection: RetrainDatasetSelectionSchema,
26110
- layout: RetrainExportLayoutSchema,
26111
- completeOnly: boolean()
26112
- }), CreateRetrainDatasetVersionResultSchema, {
26113
- kind: "mutation",
26114
- auth: "admin",
26115
- timeoutMs: 12e4
26116
- }), method(object({}), array(RetrainDatasetVersionSchema).readonly(), {
26117
- kind: "query",
26118
- auth: "admin"
26119
- }), method(object({ id: string() }), object({ deleted: boolean() }), {
26120
- kind: "mutation",
26121
- auth: "admin"
26122
- }), method(object({ id: string() }), object({
26123
- found: boolean(),
26124
- annotationHash: string(),
26125
- matches: boolean(),
26126
- missingFrames: number().int()
26127
- }), {
26128
- kind: "query",
26129
- auth: "admin",
26130
- timeoutMs: 12e4
26131
26258
  }), method(object({}), array(RetrainAssistProfileSchema).readonly(), {
26132
26259
  kind: "query",
26133
26260
  auth: "admin"
@@ -26154,6 +26281,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
26154
26281
  }), method(object({}), array(RetrainCollectionSchema).readonly(), {
26155
26282
  kind: "query",
26156
26283
  auth: "admin"
26284
+ }), method(RetrainTrainingCorpusSelectionSchema, RetrainTrainingCorpusResolveResultSchema, {
26285
+ kind: "query",
26286
+ auth: "admin",
26287
+ timeoutMs: 6e4
26157
26288
  }), method(object({
26158
26289
  id: string(),
26159
26290
  name: string().max(120)
@@ -26770,11 +26901,14 @@ var HardwareClassIdSchema = _enum([
26770
26901
  "intel-n-series",
26771
26902
  "apple-m-base",
26772
26903
  "apple-m-pro",
26904
+ "amd-phoenix",
26773
26905
  "unknown"
26774
26906
  ]);
26775
26907
  var AcceleratorKindSchema = _enum([
26776
26908
  "intel-npu",
26777
26909
  "intel-igpu",
26910
+ "amd-igpu",
26911
+ "amd-npu",
26778
26912
  "x86-cpu",
26779
26913
  "arm-cpu",
26780
26914
  "apple-ane",
@@ -28704,6 +28838,8 @@ var AgentPipelineSettingsSchema = object({
28704
28838
  * address is wrong (multi-homed host, NAT, custom interface).
28705
28839
  */
28706
28840
  reachableHost: string().optional(),
28841
+ /** Hub-persisted UI colour (`--color-node-*` theme tokens). */
28842
+ color: NodeColorIdSchema.optional(),
28707
28843
  /**
28708
28844
  * Multi-device inference opt-in (Phase 4). Per-node map deviceKey → {enabled,
28709
28845
  * weight, steps}. Absent / all-disabled ⇒ the node's single default
@@ -29253,8 +29389,9 @@ method(object({
29253
29389
  auth: "admin"
29254
29390
  }), method(object({
29255
29391
  agentNodeId: string(),
29256
- reachableHost: string().nullable()
29257
- }), object({ success: literal(true) }), {
29392
+ reachableHost: string().nullable().optional(),
29393
+ color: NodeColorIdSchema.nullable().optional()
29394
+ }).refine((v) => v.reachableHost !== void 0 || v.color !== void 0, { message: "reachableHost or color required" }), object({ success: literal(true) }), {
29258
29395
  kind: "mutation",
29259
29396
  auth: "admin"
29260
29397
  }), method(object({
@@ -32852,7 +32989,7 @@ var CustomActionInputSchema = object({
32852
32989
  action: string().min(1),
32853
32990
  input: unknown()
32854
32991
  });
32855
- method(_void(), array(AddonListItemSchema).readonly()), method(object({
32992
+ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSchema).readonly()), method(object({
32856
32993
  addonId: string(),
32857
32994
  limit: number().min(1).max(500).default(100),
32858
32995
  level: LogLevelSchema$2.optional()
@@ -33322,6 +33459,17 @@ var BatteryStatusSchema = object({
33322
33459
  */
33323
33460
  lastContactAt: number().optional(),
33324
33461
  /**
33462
+ * Ms epoch of the last COMPLETE scheduled refresh — every fact the provider
33463
+ * holds for the device (level, charging, link, device info, state slices) and
33464
+ * its cached snapshot, read together. Absent = never since this slice was
33465
+ * born. Unlike `lastUpdated` (any write, even a push of one field) this is
33466
+ * the staleness an operator can trust: the provider owns the device fact and
33467
+ * so owns the window on its age (D224, D847). Not a clock in the
33468
+ * runtime-state-durability sense: it moves once per window and must survive
33469
+ * a restart, or every restart would owe the camera a wake.
33470
+ */
33471
+ lastFullRefreshAt: number().optional(),
33472
+ /**
33325
33473
  * True when the source is a BINARY low-battery indicator (HA
33326
33474
  * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
33327
33475
  * charge level — `percentage` is then a coarse stand-in (100 = normal,
@@ -38399,6 +38547,7 @@ var OsdNamedTemplateSchema = object({
38399
38547
  });
38400
38548
  /** Where a slot's value comes from. */
38401
38549
  var OsdSourceSchema = discriminatedUnion("kind", [
38550
+ object({ kind: literal("none") }),
38402
38551
  object({
38403
38552
  kind: literal("static"),
38404
38553
  text: string().max(64)
@@ -38525,6 +38674,13 @@ method(object({ deviceId: number().int() }), object({
38525
38674
  }), object({ success: literal(true) }), {
38526
38675
  kind: "mutation",
38527
38676
  auth: "admin"
38677
+ }), method(object({
38678
+ deviceId: number().int(),
38679
+ slotId: string().min(1),
38680
+ enabled: boolean()
38681
+ }), object({ success: literal(true) }), {
38682
+ kind: "mutation",
38683
+ auth: "admin"
38528
38684
  }), method(object({
38529
38685
  sourceDeviceId: number().int(),
38530
38686
  targetDeviceId: number().int()
@@ -38956,7 +39112,15 @@ var GpuInfoSchema = object({
38956
39112
  name: string(),
38957
39113
  memoryMB: number().optional()
38958
39114
  });
38959
- var NpuInfoSchema = object({ type: _enum(["apple-ane", "intel-npu"]) });
39115
+ var NpuInfoSchema = object({
39116
+ type: _enum([
39117
+ "apple-ane",
39118
+ "intel-npu",
39119
+ "amd-xdna"
39120
+ ]),
39121
+ name: string().optional(),
39122
+ generation: _enum(["xdna1", "xdna2"]).optional()
39123
+ });
38960
39124
  var CoralInfoSchema = object({
38961
39125
  type: literal("coral-edgetpu"),
38962
39126
  bus: string().optional()
@@ -44203,9 +44367,11 @@ var zoneRulesCapability = {
44203
44367
  * once is what makes a second provider (RunPod, a local NVIDIA node) a new
44204
44368
  * manifest entry and nothing else.
44205
44369
  *
44206
- * Collection, routed by the `addonId` in every single-row input (the `llm` /
44207
- * `notification-output` posture); `describe` concat-fans across providers.
44208
- * Every method is `admin`: these spend money.
44370
+ * Collection of provider **types** (addons). Each addon owns zero or more
44371
+ * **entries** (credentials + defaults). Concat-fans: `describe`,
44372
+ * `describeTypes`, `listEntries`, `listJobs`. Single-row work is keyed by
44373
+ * `entryId`; callers also pass the owning `addonId` as the collection
44374
+ * selector (broker posture). Every method is `admin`: these spend money.
44209
44375
  *
44210
44376
  * Every cloud object a provider creates is tagged with the orchestrator's
44211
44377
  * `jobKey`, so a retried submit finds the job it already started instead of
@@ -44229,8 +44395,8 @@ var TrainingRegionOptionSchema = object({
44229
44395
  priceMultiplier: number().positive()
44230
44396
  });
44231
44397
  /**
44232
- * The provider's per-account settings, as the orchestrator reads them. They
44233
- * live in the PROVIDER addon's settings (operator decision 2026-10-08: they
44398
+ * The provider's per-entry settings, as the orchestrator reads them. They
44399
+ * live on the PROVIDER addon's entry (operator decision 2026-10-08: they
44234
44400
  * depend on the user's account), never hard-coded in the orchestrator.
44235
44401
  */
44236
44402
  var TrainingProviderDefaultsSchema = object({
@@ -44241,9 +44407,44 @@ var TrainingProviderDefaultsSchema = object({
44241
44407
  /** Hours a FAILED job's cloud dataset is kept for debugging; 0 = delete at once. */
44242
44408
  failedDatasetGraceHours: number().nonnegative()
44243
44409
  });
44410
+ /**
44411
+ * Entry form schema for a provider type — same seam as notification
44412
+ * `TargetKind.configSchema` (`z.unknown()` at the wire; callers tighten to
44413
+ * {@link ConfigUISchema}). Drives `ConfigFormBuilder` on Training → Providers.
44414
+ */
44415
+ var ConfigSchemaPassthrough = unknown();
44416
+ /** One creatable provider type, for the Add-entry dialog. */
44417
+ var TrainingProviderTypeSchema = object({
44418
+ typeId: string(),
44419
+ label: string(),
44420
+ /** Path under the addon package (e.g. `assets/training-modal/icon.svg`). */
44421
+ iconAsset: string(),
44422
+ /** Owning `training-provider` addon — collection selector for createEntry. */
44423
+ addonId: string(),
44424
+ /**
44425
+ * Fields for one entry of this type (credentials + defaults). Absent on a
44426
+ * type that only needs a name. Password fields are keychain-backed on the
44427
+ * provider addon (`secretStatePaths`), not D634 plaintext.
44428
+ */
44429
+ configSchema: ConfigSchemaPassthrough.optional()
44430
+ });
44431
+ /** One configured account / workspace under a provider type. */
44432
+ var TrainingProviderEntrySchema = object({
44433
+ entryId: string(),
44434
+ typeId: string(),
44435
+ name: string(),
44436
+ addonId: string(),
44437
+ /** Credentials are present — never the values. */
44438
+ configured: boolean()
44439
+ });
44244
44440
  var TrainingProviderDescriptorSchema = object({
44245
44441
  addonId: string(),
44246
- /** `modal`, `runpod`, `fake`, … */
44442
+ entryId: string(),
44443
+ typeId: string(),
44444
+ /** Operator-chosen entry name. */
44445
+ name: string(),
44446
+ iconAsset: string(),
44447
+ /** `modal`, `runpod`, `fake`, … — same as typeId for current providers. */
44247
44448
  kind: string(),
44248
44449
  label: string(),
44249
44450
  gpuTypes: array(TrainingGpuOptionSchema).readonly(),
@@ -44255,7 +44456,30 @@ var TrainingProviderDescriptorSchema = object({
44255
44456
  dataResidencyNote: string(),
44256
44457
  /** Credentials are present — never the values. */
44257
44458
  configured: boolean(),
44258
- defaults: TrainingProviderDefaultsSchema
44459
+ defaults: TrainingProviderDefaultsSchema,
44460
+ /**
44461
+ * Values for {@link TrainingProviderTypeSchema}'s `configSchema` — password
44462
+ * fields are keychain {@link SecretFieldView} or `null` / `''` (create),
44463
+ * never plaintext. Same hydrate contract as settings forms (D789).
44464
+ */
44465
+ formValues: record(string(), unknown()).optional()
44466
+ });
44467
+ /**
44468
+ * Patch one entry's name, credentials and defaults. Pin with owning `{ addonId }`.
44469
+ * An omitted or empty `tokenSecret` means KEEP the stored secret (D787).
44470
+ * `{ $secretClear: true }` clears it.
44471
+ */
44472
+ var TrainingProviderUpdateEntryInputSchema = object({
44473
+ entryId: string().min(1),
44474
+ name: string().min(1),
44475
+ tokenId: string().optional(),
44476
+ tokenSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
44477
+ environment: string().optional(),
44478
+ gpuType: string().optional(),
44479
+ region: string().optional(),
44480
+ costCapUsd: number().nonnegative().optional(),
44481
+ maxRuntimeSec: number().int().positive().optional(),
44482
+ failedDatasetGraceHours: number().nonnegative().optional()
44259
44483
  });
44260
44484
  var TrainingCredentialCheckSchema = object({
44261
44485
  ok: boolean(),
@@ -44307,20 +44531,33 @@ var TrainerSourceRefSchema = object({
44307
44531
  /** A commit sha (never a branch) — the job must be reproducible. */
44308
44532
  revision: string().regex(/^[0-9a-f]{7,64}$/)
44309
44533
  });
44534
+ /** Cloud job identity — route with the owning addon's `addonId` selector. */
44310
44535
  var JobRef = {
44311
- addonId: string(),
44536
+ entryId: string().min(1),
44312
44537
  jobKey: string().uuid()
44313
44538
  };
44314
44539
  var TrainingLogLineSchema = object({
44315
44540
  ts: number(),
44316
44541
  text: string()
44317
44542
  });
44318
- method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "admin" }), method(object({ addonId: string() }), TrainingCredentialCheckSchema, {
44543
+ method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderTypeSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderEntrySchema).readonly(), { auth: "admin" }), method(object({
44544
+ typeId: string().min(1),
44545
+ name: string().min(1)
44546
+ }), object({ entryId: string() }), {
44547
+ kind: "mutation",
44548
+ auth: "admin"
44549
+ }), method(object({ entryId: string().min(1) }), object({ ok: literal(true) }), {
44550
+ kind: "mutation",
44551
+ auth: "admin"
44552
+ }), method(TrainingProviderUpdateEntryInputSchema, object({ ok: literal(true) }), {
44553
+ kind: "mutation",
44554
+ auth: "admin"
44555
+ }), method(object({ entryId: string().min(1) }), TrainingCredentialCheckSchema, {
44319
44556
  kind: "mutation",
44320
44557
  auth: "admin",
44321
44558
  timeoutMs: 3e4
44322
44559
  }), method(object({
44323
- addonId: string(),
44560
+ entryId: string().min(1),
44324
44561
  gpuType: string(),
44325
44562
  region: string(),
44326
44563
  datasetBytes: number().int().nonnegative(),
@@ -44386,7 +44623,7 @@ method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "
44386
44623
  kind: "mutation",
44387
44624
  auth: "admin",
44388
44625
  timeoutMs: 5 * 6e4
44389
- }), method(object({ addonId: string() }), array(ProviderJobStatusSchema).readonly(), {
44626
+ }), method(object({ entryId: string().min(1).optional() }), array(ProviderJobStatusSchema).readonly(), {
44390
44627
  auth: "admin",
44391
44628
  timeoutMs: 2 * 6e4
44392
44629
  });
@@ -44396,20 +44633,31 @@ method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "
44396
44633
  *
44397
44634
  * It owns what every provider must agree on, written once:
44398
44635
  *
44399
- * - **Dataset versions.** Taken over from pipeline-analytics (D796): the rows
44400
- * live here (D808); pipeline-analytics still CUTS a version (it owns the corpus and
44401
- * the export plan) and reaches this store through the five row methods
44402
- * below, so the Retrain page and the export did not change.
44403
44636
  * - **Jobs.** A persisted state machine, a reconcile loop against the
44404
44637
  * provider, a cost guard, a daily orphan sweep and the retention rules.
44405
44638
  * - **The return flow.** Verified artefacts → Model Studio
44406
44639
  * (`custom-model-registry.importModelBundle`) → canary on cameras →
44407
44640
  * rollback. Nothing is ever registered automatically.
44408
44641
  *
44642
+ * Plan/start takes a live {@link TrainingCorpusSelection} (collections +
44643
+ * work-state rule); frame ids are resolved via pipeline-analytics at that
44644
+ * moment and snapshotted on the job. No frozen dataset version.
44645
+ *
44409
44646
  * Every method is `admin`.
44410
44647
  */
44411
- /** A version WITH its frame list — what the export and a job read. */
44412
- var TrainingDatasetVersionRowSchema = RetrainDatasetVersionSchema.omit({ frameCount: true }).extend({ frameIds: array(string()).max(2e4).readonly() });
44648
+ /**
44649
+ * Live corpus criteria for plan/start (design §5.2). Default is complete-only.
44650
+ * Frame ids are resolved at plan/start — never a frozen version id.
44651
+ */
44652
+ var TrainingCorpusSelectionSchema = object({
44653
+ collectionIds: array(string()).min(1),
44654
+ /** Default complete-only for v1 */
44655
+ workState: _enum([
44656
+ "complete",
44657
+ "annotated_or_complete",
44658
+ "any_annotated"
44659
+ ]).default("complete")
44660
+ });
44413
44661
  /** A base model a detector job can fine-tune, and the baseline it is judged against. */
44414
44662
  var TrainingBaseModelSchema = object({
44415
44663
  modelId: string(),
@@ -44427,9 +44675,13 @@ var TrainingPresetSchema = _enum([
44427
44675
  "thorough"
44428
44676
  ]);
44429
44677
  var TrainingJobRequestSchema = object({
44430
- datasetVersionId: string().min(1),
44431
- /** The `training-provider` addon that runs it. */
44432
- addonId: string().min(1),
44678
+ /** Live collection + work-state criteria; resolved at plan/start. */
44679
+ selection: TrainingCorpusSelectionSchema,
44680
+ /**
44681
+ * The provider **entry** that runs it (credentials + defaults).
44682
+ * Resolve to an owning addon via `training-provider.describe` / `listEntries`.
44683
+ */
44684
+ entryId: string().min(1),
44433
44685
  gpuType: string().min(1),
44434
44686
  region: string(),
44435
44687
  baseModelId: string().min(1),
@@ -44488,6 +44740,7 @@ var TrainingJobPlanSchema = object({
44488
44740
  * result lives after the operator acted on it.
44489
44741
  */
44490
44742
  var TrainingJobStateSchema = _enum([
44743
+ "queued",
44491
44744
  "uploading",
44492
44745
  "submitted",
44493
44746
  "starting",
@@ -44541,6 +44794,11 @@ var TrainingJobSchema = object({
44541
44794
  createdAt: number(),
44542
44795
  updatedAt: number(),
44543
44796
  state: TrainingJobStateSchema,
44797
+ /**
44798
+ * Shared by every job enqueued together via `startJobBatch`.
44799
+ * `null` for a lone `startJob`. The engine advances queued siblings by this id.
44800
+ */
44801
+ batchId: string().nullable().default(null),
44544
44802
  stage: TrainingStageSchema.nullable(),
44545
44803
  failure: object({
44546
44804
  reason: TrainingFailureReasonSchema,
@@ -44552,6 +44810,12 @@ var TrainingJobSchema = object({
44552
44810
  maxRuntimeSec: number().int().positive(),
44553
44811
  trainer: TrainerSourceRefSchema,
44554
44812
  datasetLayout: RetrainExportLayoutSchema,
44813
+ /** Frame count snapshotted when the job started (audit; equals `frameIds.length`). */
44814
+ frameCount: number().int().nonnegative(),
44815
+ /** Resolved frame ids at start — the archive ships exactly these. */
44816
+ frameIds: array(string()).max(2e4).readonly(),
44817
+ /** Hash of what those frames taught at start (trainer `dataset.annotationHash`). */
44818
+ annotationHash: string(),
44555
44819
  dataset: object({
44556
44820
  bytes: number().int(),
44557
44821
  sha256: string(),
@@ -44593,13 +44857,7 @@ var TrainingJobSchema = object({
44593
44857
  canary: TrainingCanarySchema.nullable()
44594
44858
  });
44595
44859
  var IdInput = object({ id: string().min(1) });
44596
- method(TrainingDatasetVersionRowSchema, object({ stored: boolean() }), {
44597
- kind: "mutation",
44598
- auth: "admin"
44599
- }), method(IdInput, TrainingDatasetVersionRowSchema.nullable(), { auth: "admin" }), method(object({}), array(RetrainDatasetVersionSchema).readonly(), { auth: "admin" }), method(object({ name: string() }), object({ id: string().nullable() }), { auth: "admin" }), method(IdInput, object({ removed: boolean() }), {
44600
- kind: "mutation",
44601
- auth: "admin"
44602
- }), method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" }), method(TrainingJobRequestSchema, TrainingJobPlanSchema, {
44860
+ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" }), method(TrainingJobRequestSchema, TrainingJobPlanSchema, {
44603
44861
  auth: "admin",
44604
44862
  timeoutMs: 6e4
44605
44863
  }), method(TrainingJobRequestSchema.extend({
@@ -44618,6 +44876,24 @@ method(TrainingDatasetVersionRowSchema, object({ stored: boolean() }), {
44618
44876
  kind: "mutation",
44619
44877
  auth: "admin",
44620
44878
  timeoutMs: 12e4
44879
+ }), method(TrainingJobRequestSchema.omit({ baseModelId: true }).extend({
44880
+ baseModelIds: array(string().min(1)).min(1),
44881
+ name: string().min(1).max(120),
44882
+ maxCostUsd: number().positive(),
44883
+ maxRuntimeSec: number().int().positive(),
44884
+ idempotencyKey: string().min(8).max(80)
44885
+ }), discriminatedUnion("kind", [object({
44886
+ kind: literal("started"),
44887
+ batchId: string(),
44888
+ jobs: array(TrainingJobSchema).min(1).readonly()
44889
+ }), object({
44890
+ kind: literal("refused"),
44891
+ reason: TrainingRefusalReasonSchema,
44892
+ detail: string()
44893
+ })]), {
44894
+ kind: "mutation",
44895
+ auth: "admin",
44896
+ timeoutMs: 12e4
44621
44897
  }), method(object({}), array(TrainingJobSchema).readonly(), { auth: "admin" }), method(IdInput, TrainingJobSchema.nullable(), { auth: "admin" }), method(IdInput, TrainingJobSchema, {
44622
44898
  kind: "mutation",
44623
44899
  auth: "admin",
@@ -46228,17 +46504,6 @@ var BaseDeviceProvider = class extends BaseAddon {
46228
46504
  return toDeviceSummary(device, this.addonId);
46229
46505
  }
46230
46506
  };
46231
- /**
46232
- * Default silence budget before a sleeping battery camera is called gone.
46233
- *
46234
- * Sized off the mechanism that produces contact, not off taste: a battery cam
46235
- * on this deployment surfaces a firmware push (battery level, sleep/wake, or a
46236
- * motion email) on the order of hours even with no visitors, and the snapshot
46237
- * wrapper's own battery window is 1 hour. Six hours is therefore several
46238
- * missed opportunities, not one — so a single quiet night does not raise a
46239
- * fault, and a genuinely flat camera is named well inside a day.
46240
- */
46241
- var BATTERY_UNREACHABLE_AFTER_MS = 360 * 6e4;
46242
46507
  DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
46243
46508
  new Set(Object.values(DeviceType));
46244
46509
  Object.freeze({
@@ -50202,6 +50467,36 @@ Object.freeze({
50202
50467
  addonId: null,
50203
50468
  access: "view"
50204
50469
  },
50470
+ "oidcConnections.connectionFormSchema": {
50471
+ capName: "oidc-connections",
50472
+ capScope: "system",
50473
+ addonId: null,
50474
+ access: "view"
50475
+ },
50476
+ "oidcConnections.createConnection": {
50477
+ capName: "oidc-connections",
50478
+ capScope: "system",
50479
+ addonId: null,
50480
+ access: "create"
50481
+ },
50482
+ "oidcConnections.deleteConnection": {
50483
+ capName: "oidc-connections",
50484
+ capScope: "system",
50485
+ addonId: null,
50486
+ access: "delete"
50487
+ },
50488
+ "oidcConnections.listConnections": {
50489
+ capName: "oidc-connections",
50490
+ capScope: "system",
50491
+ addonId: null,
50492
+ access: "view"
50493
+ },
50494
+ "oidcConnections.updateConnection": {
50495
+ capName: "oidc-connections",
50496
+ capScope: "system",
50497
+ addonId: null,
50498
+ access: "create"
50499
+ },
50205
50500
  "osd.getStatus": {
50206
50501
  capName: "osd",
50207
50502
  capScope: "device",
@@ -50274,6 +50569,12 @@ Object.freeze({
50274
50569
  addonId: null,
50275
50570
  access: "create"
50276
50571
  },
50572
+ "osdManager.setSlotCameraEnabled": {
50573
+ capName: "osd-manager",
50574
+ capScope: "system",
50575
+ addonId: null,
50576
+ access: "create"
50577
+ },
50277
50578
  "osdManager.upsertTemplate": {
50278
50579
  capName: "osd-manager",
50279
50580
  capScope: "system",
@@ -50412,12 +50713,6 @@ Object.freeze({
50412
50713
  addonId: null,
50413
50714
  access: "create"
50414
50715
  },
50415
- "pipelineAnalytics.createRetrainDatasetVersion": {
50416
- capName: "pipeline-analytics",
50417
- capScope: "device",
50418
- addonId: null,
50419
- access: "create"
50420
- },
50421
50716
  "pipelineAnalytics.deleteDeviceEvents": {
50422
50717
  capName: "pipeline-analytics",
50423
50718
  capScope: "device",
@@ -50430,12 +50725,6 @@ Object.freeze({
50430
50725
  addonId: null,
50431
50726
  access: "delete"
50432
50727
  },
50433
- "pipelineAnalytics.deleteRetrainDatasetVersion": {
50434
- capName: "pipeline-analytics",
50435
- capScope: "device",
50436
- addonId: null,
50437
- access: "delete"
50438
- },
50439
50728
  "pipelineAnalytics.deleteTracks": {
50440
50729
  capName: "pipeline-analytics",
50441
50730
  capScope: "device",
@@ -50538,6 +50827,12 @@ Object.freeze({
50538
50827
  addonId: null,
50539
50828
  access: "view"
50540
50829
  },
50830
+ "pipelineAnalytics.getRetrainCorpusKpi": {
50831
+ capName: "pipeline-analytics",
50832
+ capScope: "device",
50833
+ addonId: null,
50834
+ access: "view"
50835
+ },
50541
50836
  "pipelineAnalytics.getRetrainExportUrl": {
50542
50837
  capName: "pipeline-analytics",
50543
50838
  capScope: "device",
@@ -50658,7 +50953,7 @@ Object.freeze({
50658
50953
  addonId: null,
50659
50954
  access: "view"
50660
50955
  },
50661
- "pipelineAnalytics.listRetrainDatasetVersions": {
50956
+ "pipelineAnalytics.listRetrainCorpusPage": {
50662
50957
  capName: "pipeline-analytics",
50663
50958
  capScope: "device",
50664
50959
  addonId: null,
@@ -50712,12 +51007,6 @@ Object.freeze({
50712
51007
  addonId: null,
50713
51008
  access: "create"
50714
51009
  },
50715
- "pipelineAnalytics.previewRetrainDataset": {
50716
- capName: "pipeline-analytics",
50717
- capScope: "device",
50718
- addonId: null,
50719
- access: "view"
50720
- },
50721
51010
  "pipelineAnalytics.proposeRetrainAnnotations": {
50722
51011
  capName: "pipeline-analytics",
50723
51012
  capScope: "device",
@@ -50796,6 +51085,12 @@ Object.freeze({
50796
51085
  addonId: null,
50797
51086
  access: "create"
50798
51087
  },
51088
+ "pipelineAnalytics.resolveRetrainTrainingCorpus": {
51089
+ capName: "pipeline-analytics",
51090
+ capScope: "device",
51091
+ addonId: null,
51092
+ access: "view"
51093
+ },
50799
51094
  "pipelineAnalytics.restageRetrainTrack": {
50800
51095
  capName: "pipeline-analytics",
50801
51096
  capScope: "device",
@@ -50856,12 +51151,6 @@ Object.freeze({
50856
51151
  addonId: null,
50857
51152
  access: "create"
50858
51153
  },
50859
- "pipelineAnalytics.verifyRetrainDatasetVersion": {
50860
- capName: "pipeline-analytics",
50861
- capScope: "device",
50862
- addonId: null,
50863
- access: "view"
50864
- },
50865
51154
  "pipelineAnalytics.wipeAllAnalytics": {
50866
51155
  capName: "pipeline-analytics",
50867
51156
  capScope: "device",
@@ -53256,18 +53545,6 @@ Object.freeze({
53256
53545
  addonId: null,
53257
53546
  access: "create"
53258
53547
  },
53259
- "training.findDatasetVersionIdByName": {
53260
- capName: "training",
53261
- capScope: "system",
53262
- addonId: null,
53263
- access: "view"
53264
- },
53265
- "training.getDatasetVersion": {
53266
- capName: "training",
53267
- capScope: "system",
53268
- addonId: null,
53269
- access: "view"
53270
- },
53271
53548
  "training.getJob": {
53272
53549
  capName: "training",
53273
53550
  capScope: "system",
@@ -53286,12 +53563,6 @@ Object.freeze({
53286
53563
  addonId: null,
53287
53564
  access: "view"
53288
53565
  },
53289
- "training.listDatasetVersions": {
53290
- capName: "training",
53291
- capScope: "system",
53292
- addonId: null,
53293
- access: "view"
53294
- },
53295
53566
  "training.listJobs": {
53296
53567
  capName: "training",
53297
53568
  capScope: "system",
@@ -53304,54 +53575,66 @@ Object.freeze({
53304
53575
  addonId: null,
53305
53576
  access: "view"
53306
53577
  },
53307
- "training.putDatasetVersion": {
53578
+ "training.registerResult": {
53308
53579
  capName: "training",
53309
53580
  capScope: "system",
53310
53581
  addonId: null,
53311
53582
  access: "create"
53312
53583
  },
53313
- "training.registerResult": {
53584
+ "training.rollbackCanary": {
53314
53585
  capName: "training",
53315
53586
  capScope: "system",
53316
53587
  addonId: null,
53317
53588
  access: "create"
53318
53589
  },
53319
- "training.removeDatasetVersion": {
53590
+ "training.startJob": {
53320
53591
  capName: "training",
53321
53592
  capScope: "system",
53322
53593
  addonId: null,
53323
- access: "delete"
53594
+ access: "create"
53324
53595
  },
53325
- "training.rollbackCanary": {
53596
+ "training.startJobBatch": {
53326
53597
  capName: "training",
53327
53598
  capScope: "system",
53328
53599
  addonId: null,
53329
53600
  access: "create"
53330
53601
  },
53331
- "training.startJob": {
53332
- capName: "training",
53602
+ "trainingProvider.cancelJob": {
53603
+ capName: "training-provider",
53333
53604
  capScope: "system",
53334
53605
  addonId: null,
53335
53606
  access: "create"
53336
53607
  },
53337
- "trainingProvider.cancelJob": {
53608
+ "trainingProvider.cleanupJob": {
53338
53609
  capName: "training-provider",
53339
53610
  capScope: "system",
53340
53611
  addonId: null,
53341
53612
  access: "create"
53342
53613
  },
53343
- "trainingProvider.cleanupJob": {
53614
+ "trainingProvider.createEntry": {
53344
53615
  capName: "training-provider",
53345
53616
  capScope: "system",
53346
53617
  addonId: null,
53347
53618
  access: "create"
53348
53619
  },
53620
+ "trainingProvider.deleteEntry": {
53621
+ capName: "training-provider",
53622
+ capScope: "system",
53623
+ addonId: null,
53624
+ access: "delete"
53625
+ },
53349
53626
  "trainingProvider.describe": {
53350
53627
  capName: "training-provider",
53351
53628
  capScope: "system",
53352
53629
  addonId: null,
53353
53630
  access: "view"
53354
53631
  },
53632
+ "trainingProvider.describeTypes": {
53633
+ capName: "training-provider",
53634
+ capScope: "system",
53635
+ addonId: null,
53636
+ access: "view"
53637
+ },
53355
53638
  "trainingProvider.estimateJob": {
53356
53639
  capName: "training-provider",
53357
53640
  capScope: "system",
@@ -53364,6 +53647,12 @@ Object.freeze({
53364
53647
  addonId: null,
53365
53648
  access: "view"
53366
53649
  },
53650
+ "trainingProvider.listEntries": {
53651
+ capName: "training-provider",
53652
+ capScope: "system",
53653
+ addonId: null,
53654
+ access: "view"
53655
+ },
53367
53656
  "trainingProvider.listJobEvents": {
53368
53657
  capName: "training-provider",
53369
53658
  capScope: "system",
@@ -53394,6 +53683,12 @@ Object.freeze({
53394
53683
  addonId: null,
53395
53684
  access: "create"
53396
53685
  },
53686
+ "trainingProvider.updateEntry": {
53687
+ capName: "training-provider",
53688
+ capScope: "system",
53689
+ addonId: null,
53690
+ access: "create"
53691
+ },
53397
53692
  "trainingProvider.uploadDataset": {
53398
53693
  capName: "training-provider",
53399
53694
  capScope: "system",
@@ -56752,6 +57047,11 @@ Object.freeze({
56752
57047
  form: "single",
56753
57048
  optional: false
56754
57049
  }],
57050
+ "osdManager.setSlotCameraEnabled": [{
57051
+ name: "deviceId",
57052
+ form: "single",
57053
+ optional: false
57054
+ }],
56755
57055
  "petFeeder.callPet": [{
56756
57056
  name: "deviceId",
56757
57057
  form: "single",
@@ -56972,6 +57272,11 @@ Object.freeze({
56972
57272
  form: "array",
56973
57273
  optional: false
56974
57274
  }],
57275
+ "pipelineAnalytics.listRetrainCorpusPage": [{
57276
+ name: "deviceIds",
57277
+ form: "array",
57278
+ optional: true
57279
+ }],
56975
57280
  "pipelineAnalytics.listRetrainStaging": [{
56976
57281
  name: "deviceIds",
56977
57282
  form: "array",
@@ -58081,6 +58386,7 @@ Object.freeze({
58081
58386
  });
58082
58387
  Object.freeze({
58083
58388
  "broker": "broker",
58389
+ "calendar-source": "calendar",
58084
58390
  "device-export": "device-export",
58085
58391
  "llm": "ai",
58086
58392
  "mesh-network": "mesh",
@@ -58205,6 +58511,9 @@ new Map(Object.entries({
58205
58511
  "stream-online": "device-events",
58206
58512
  "stream-offline": "device-events",
58207
58513
  "detection-blind": "device-events",
58514
+ "recording-stalled": "device-events",
58515
+ "recording-healthy": "device-events",
58516
+ "recording-degraded": "device-events",
58208
58517
  "node-online": "system",
58209
58518
  "node-offline": "system",
58210
58519
  "node-restarting": "system",
@@ -58218,6 +58527,9 @@ new Map(Object.entries({
58218
58527
  "wrapper-update-available": "system",
58219
58528
  "backup-completed": "system",
58220
58529
  "export-completed": "system",
58530
+ "storage-location-low": "system",
58531
+ "storage-location-critical": "system",
58532
+ "storage-location-ok": "system",
58221
58533
  "alarm-triggered": "alarm",
58222
58534
  "alarm-armed": "alarm",
58223
58535
  "alarm-disarmed": "alarm",
@@ -58279,6 +58591,16 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
58279
58591
  label: "Detecting nothing",
58280
58592
  kinds: ["detection-blind"]
58281
58593
  },
58594
+ {
58595
+ id: "recording",
58596
+ family: "device-events",
58597
+ label: "Recording",
58598
+ kinds: [
58599
+ "recording-stalled",
58600
+ "recording-healthy",
58601
+ "recording-degraded"
58602
+ ]
58603
+ },
58282
58604
  {
58283
58605
  id: "nodes",
58284
58606
  family: "system",
@@ -58319,6 +58641,16 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
58319
58641
  label: "Backups & exports",
58320
58642
  kinds: ["backup-completed", "export-completed"]
58321
58643
  },
58644
+ {
58645
+ id: "storage",
58646
+ family: "system",
58647
+ label: "Storage locations",
58648
+ kinds: [
58649
+ "storage-location-low",
58650
+ "storage-location-critical",
58651
+ "storage-location-ok"
58652
+ ]
58653
+ },
58322
58654
  {
58323
58655
  id: "alarm",
58324
58656
  family: "alarm",
@@ -58401,6 +58733,9 @@ var DEVICE_EVENTS = [
58401
58733
  "stream-online",
58402
58734
  "stream-offline",
58403
58735
  "detection-blind",
58736
+ "recording-stalled",
58737
+ "recording-healthy",
58738
+ "recording-degraded",
58404
58739
  ...ALARM
58405
58740
  ];
58406
58741
  /** Every kind a summary may speak about — the two summarisable families. */
@@ -58568,6 +58903,11 @@ var STATE_SUMMARY_VARS = [
58568
58903
  [...RULE_TEXT],
58569
58904
  [...RULE_TEXT],
58570
58905
  [...RULE_TEXT],
58906
+ [...RULE_TEXT],
58907
+ [...RULE_TEXT],
58908
+ [...RULE_TEXT],
58909
+ [...RULE_TEXT],
58910
+ [...RULE_TEXT],
58571
58911
  ([...RULE_TEXT], [...UPDATES]),
58572
58912
  ([...RULE_TEXT], [...PACKAGE_UPDATES]),
58573
58913
  ([...RULE_TEXT], [...PACKAGE_UPDATES]),
@@ -71239,7 +71579,7 @@ var require_snapshot_utils = /* @__PURE__ */ __commonJSMin(((exports, module) =>
71239
71579
  //#region ../../node_modules/undici/lib/mock/snapshot-recorder.js
71240
71580
  var require_snapshot_recorder = /* @__PURE__ */ __commonJSMin(((exports, module) => {
71241
71581
  var { writeFile: writeFile$1, readFile: readFile$1, mkdir: mkdir$2 } = __require("node:fs/promises");
71242
- var { dirname: dirname$1, resolve } = __require("node:path");
71582
+ var { dirname: dirname$2, resolve: resolve$1 } = __require("node:path");
71243
71583
  var { setTimeout: setTimeout$1, clearTimeout: clearTimeout$1 } = __require("node:timers");
71244
71584
  var { InvalidArgumentError, UndiciError } = require_errors$1();
71245
71585
  var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -71504,7 +71844,7 @@ var require_snapshot_recorder = /* @__PURE__ */ __commonJSMin(((exports, module)
71504
71844
  const path = filePath || this.#snapshotPath;
71505
71845
  if (!path) throw new InvalidArgumentError("Snapshot path is required");
71506
71846
  try {
71507
- const data = await readFile$1(resolve(path), "utf8");
71847
+ const data = await readFile$1(resolve$1(path), "utf8");
71508
71848
  const parsed = JSON.parse(data);
71509
71849
  if (Array.isArray(parsed)) {
71510
71850
  this.#snapshots.clear();
@@ -71524,8 +71864,8 @@ var require_snapshot_recorder = /* @__PURE__ */ __commonJSMin(((exports, module)
71524
71864
  async saveSnapshots(filePath) {
71525
71865
  const path = filePath || this.#snapshotPath;
71526
71866
  if (!path) throw new InvalidArgumentError("Snapshot path is required");
71527
- const resolvedPath = resolve(path);
71528
- await mkdir$2(dirname$1(resolvedPath), { recursive: true });
71867
+ const resolvedPath = resolve$1(path);
71868
+ await mkdir$2(dirname$2(resolvedPath), { recursive: true });
71529
71869
  const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({
71530
71870
  hash,
71531
71871
  snapshot
@@ -160162,6 +160502,7 @@ function buildInitialStatus2(config, enabled) {
160162
160502
  lastErrorMessage: void 0
160163
160503
  };
160164
160504
  }
160505
+ process.platform, process.env["SystemRoot"];
160165
160506
  /**
160166
160507
  * The spawn a one-shot ffmpeg site uses: the spec's seam when one is injected,
160167
160508
  * else the node's PINNED binary (D540), else `null` — and the site refuses by
@@ -160734,6 +161075,13 @@ var reolinkCameraSchema = object({
160734
161075
  */
160735
161076
  clipCatalogForcedWake: boolean().optional(),
160736
161077
  /**
161078
+ * Battery cameras only: the window, in minutes, in which the camera refreshes
161079
+ * ALL its metadata and its cached snapshot exactly once (D847). Absent means
161080
+ * the default (60); `0` turns the scheduled refresh off for this camera; a
161081
+ * value is clamped to 15..240 (below the 6 h presence budget).
161082
+ */
161083
+ batteryRefreshMinutes: number().int().min(0).max(240).optional(),
161084
+ /**
160737
161085
  * Per-AI-class sensitivity overrides — pushed via
160738
161086
  * `setAiDetection(aiType, sensitivity)` (cmdId=343). Reolink
160739
161087
  * range 0..100 — same scale the reolink_aio CGI exposes
@@ -166991,14 +167339,266 @@ function deriveChargingSource(adapterStatus, chargeStatus) {
166991
167339
  function reportsPowerSource(info) {
166992
167340
  return info.adapterStatus !== void 0 || info.chargeStatus !== void 0;
166993
167341
  }
167342
+ /** The settings key, per device. */
167343
+ var BATTERY_REFRESH_MINUTES_KEY = "batteryRefreshMinutes";
167344
+ /** How often the in-memory check runs. Costs nothing: no I/O unless it fires. */
167345
+ var BATTERY_REFRESH_TICK_MS = 6e4;
167346
+ /** The least time between two attempts in one window. */
167347
+ var RETRY_BACKOFF_MS = 10 * 6e4;
167348
+ var MAX_WAKE_LEAD_MS = 5 * 6e4;
167349
+ var STAGGER_SLOTS = 60;
167350
+ /** The operator's window in ms, or `0` for off. Out-of-range values clamp. */
167351
+ function resolveBatteryRefreshIntervalMs(configured) {
167352
+ if (typeof configured !== "number" || !Number.isFinite(configured)) return 60 * 6e4;
167353
+ if (configured <= 0) return 0;
167354
+ return Math.min(240, Math.max(15, Math.round(configured))) * 6e4;
167355
+ }
167356
+ /** Deterministic per-device offset so a fleet's windows do not align. */
167357
+ function staggerOffsetMs(deviceId, intervalMs) {
167358
+ const slot = Math.abs(Math.trunc(deviceId)) % STAGGER_SLOTS;
167359
+ return Math.floor(intervalMs / STAGGER_SLOTS * slot);
167360
+ }
167361
+ /** Which window a wall-clock instant falls in. */
167362
+ function windowIndex(at, intervalMs, offsetMs) {
167363
+ return Math.floor((at - offsetMs) / intervalMs);
167364
+ }
167365
+ /** How long before a window's end the wake is allowed to fire. */
167366
+ function wakeLeadMs(intervalMs) {
167367
+ return Math.min(MAX_WAKE_LEAD_MS, Math.floor(intervalMs / 10));
167368
+ }
167369
+ function decideBatteryRefresh(input) {
167370
+ if (input.intervalMs <= 0) return {
167371
+ kind: "skip",
167372
+ reason: "off"
167373
+ };
167374
+ const current = windowIndex(input.nowMs, input.intervalMs, input.offsetMs);
167375
+ if ((typeof input.lastFullRefreshAt === "number" && input.lastFullRefreshAt > 0 ? windowIndex(input.lastFullRefreshAt, input.intervalMs, input.offsetMs) : Number.NEGATIVE_INFINITY) >= current) return {
167376
+ kind: "skip",
167377
+ reason: "done"
167378
+ };
167379
+ if (input.attemptsInWindow >= 2) return {
167380
+ kind: "skip",
167381
+ reason: "attempts-exhausted"
167382
+ };
167383
+ if (input.lastAttemptAt !== void 0 && input.attemptsInWindow > 0 && input.nowMs - input.lastAttemptAt < 6e5) return {
167384
+ kind: "skip",
167385
+ reason: "backoff"
167386
+ };
167387
+ if (input.awake) return { kind: "refresh-awake" };
167388
+ const windowEnd = input.offsetMs + (current + 1) * input.intervalMs;
167389
+ if (input.nowMs < windowEnd - wakeLeadMs(input.intervalMs)) return {
167390
+ kind: "skip",
167391
+ reason: "asleep-before-deadline"
167392
+ };
167393
+ return { kind: "wake-then-refresh" };
167394
+ }
167395
+ var BatteryHourlyRefresh = class {
167396
+ deps;
167397
+ inFlight = null;
167398
+ attemptWindow = Number.NEGATIVE_INFINITY;
167399
+ attempts = 0;
167400
+ lastAttemptAt;
167401
+ constructor(deps) {
167402
+ this.deps = deps;
167403
+ }
167404
+ /**
167405
+ * The periodic check. Resolves `true` when a refresh landed in this call.
167406
+ * Never throws.
167407
+ */
167408
+ async tick() {
167409
+ return this.consider("tick");
167410
+ }
167411
+ /**
167412
+ * The camera was just observed coming up (event, sleep poll, hub summary, a
167413
+ * confirmed wake). Resolves `true` when this call refreshed everything, so
167414
+ * the caller can skip the parts of its own fan-out the refresh already did.
167415
+ */
167416
+ async noteAwake() {
167417
+ return this.consider("wake-transition");
167418
+ }
167419
+ async consider(trigger) {
167420
+ if (this.inFlight !== null) return this.inFlight;
167421
+ const nowMs = this.deps.now();
167422
+ const intervalMs = this.deps.intervalMs();
167423
+ const offsetMs = intervalMs > 0 ? staggerOffsetMs(this.deps.deviceId, intervalMs) : 0;
167424
+ const window = intervalMs > 0 ? windowIndex(nowMs, intervalMs, offsetMs) : 0;
167425
+ if (window !== this.attemptWindow) {
167426
+ this.attemptWindow = window;
167427
+ this.attempts = 0;
167428
+ this.lastAttemptAt = void 0;
167429
+ }
167430
+ const decision = decideBatteryRefresh({
167431
+ nowMs,
167432
+ intervalMs,
167433
+ offsetMs,
167434
+ lastFullRefreshAt: this.deps.lastFullRefreshAt(),
167435
+ awake: trigger === "wake-transition" || this.deps.isAwake(),
167436
+ attemptsInWindow: this.attempts,
167437
+ lastAttemptAt: this.lastAttemptAt
167438
+ });
167439
+ if (decision.kind === "skip") {
167440
+ this.logSkip(decision.reason, trigger);
167441
+ return false;
167442
+ }
167443
+ this.attempts += 1;
167444
+ this.lastAttemptAt = nowMs;
167445
+ const run = this.run(decision.kind, trigger).finally(() => {
167446
+ this.inFlight = null;
167447
+ });
167448
+ this.inFlight = run;
167449
+ return run;
167450
+ }
167451
+ async run(kind, trigger) {
167452
+ const { logger, deviceId } = this.deps;
167453
+ try {
167454
+ if (kind === "wake-then-refresh") {
167455
+ logger.info("battery hourly refresh: not awake in the window — waking once", {
167456
+ tags: { deviceId },
167457
+ meta: { attempt: this.attempts }
167458
+ });
167459
+ if (!await this.deps.wake()) {
167460
+ logger.warn("battery hourly refresh: the wake did not bring the camera up", {
167461
+ tags: { deviceId },
167462
+ meta: {
167463
+ attempt: this.attempts,
167464
+ retryAfterMs: RETRY_BACKOFF_MS
167465
+ }
167466
+ });
167467
+ return false;
167468
+ }
167469
+ if (this.windowSatisfied()) return true;
167470
+ } else logger.info("battery hourly refresh: camera is awake — refreshing now, no wake", {
167471
+ tags: { deviceId },
167472
+ meta: {
167473
+ trigger,
167474
+ attempt: this.attempts
167475
+ }
167476
+ });
167477
+ const outcome = await this.deps.refreshAll();
167478
+ if (outcome.failed.length > 0) {
167479
+ logger.warn("battery hourly refresh incomplete — not stamped, retried at the next chance", {
167480
+ tags: { deviceId },
167481
+ meta: {
167482
+ failed: outcome.failed,
167483
+ attempt: this.attempts,
167484
+ retryAfterMs: RETRY_BACKOFF_MS
167485
+ }
167486
+ });
167487
+ return false;
167488
+ }
167489
+ const at = this.deps.now();
167490
+ this.deps.stamp(at);
167491
+ logger.info("battery hourly refresh complete", {
167492
+ tags: { deviceId },
167493
+ meta: {
167494
+ kind,
167495
+ trigger,
167496
+ at
167497
+ }
167498
+ });
167499
+ return true;
167500
+ } catch (err) {
167501
+ logger.warn("battery hourly refresh failed — retried at the next chance", {
167502
+ tags: { deviceId },
167503
+ meta: {
167504
+ error: err instanceof Error ? err.message : String(err),
167505
+ attempt: this.attempts,
167506
+ retryAfterMs: RETRY_BACKOFF_MS
167507
+ }
167508
+ });
167509
+ return false;
167510
+ }
167511
+ }
167512
+ windowSatisfied() {
167513
+ const intervalMs = this.deps.intervalMs();
167514
+ if (intervalMs <= 0) return false;
167515
+ const offsetMs = staggerOffsetMs(this.deps.deviceId, intervalMs);
167516
+ const last = this.deps.lastFullRefreshAt();
167517
+ if (typeof last !== "number" || last <= 0) return false;
167518
+ return windowIndex(last, intervalMs, offsetMs) >= windowIndex(this.deps.now(), intervalMs, offsetMs);
167519
+ }
167520
+ logSkip(reason, trigger) {
167521
+ if (reason === "backoff" || reason === "attempts-exhausted") {
167522
+ this.deps.logger.info("battery hourly refresh skipped", {
167523
+ tags: { deviceId: this.deps.deviceId },
167524
+ meta: {
167525
+ reason,
167526
+ trigger,
167527
+ attempts: this.attempts
167528
+ }
167529
+ });
167530
+ return;
167531
+ }
167532
+ this.deps.logger.debug("battery hourly refresh not due", {
167533
+ tags: { deviceId: this.deps.deviceId },
167534
+ meta: {
167535
+ reason,
167536
+ trigger
167537
+ }
167538
+ });
167539
+ }
167540
+ };
167541
+ //#endregion
167542
+ //#region src/battery-refresh-settings.ts
167543
+ var BATTERY_REFRESH_SECTION_ID = "battery-refresh";
167544
+ var BATTERY_REFRESH_MINUTES_DESCRIPTION = `Once per window this camera refreshes everything CamStack knows about it — battery level and charging, signal, device info, every state panel and the cached snapshot. If the camera is awake at some point in the window (a motion event, a live view, anything that woke it) it is refreshed then and not woken again; if the whole window passes with the camera asleep, it is woken ONCE near the end of the window, refreshed, and left to go back to sleep. A camera that does not answer is retried at most twice per window. 15–240 minutes, default 60; 0 turns the scheduled refresh off (the battery level then ages until the camera next wakes by itself).`;
167545
+ /** Battery cameras only: a mains camera keeps its facts fresh on its own socket. */
167546
+ function buildBatteryRefreshDeviceSection(input) {
167547
+ if (!input.isBattery) return [];
167548
+ return [{
167549
+ id: BATTERY_REFRESH_SECTION_ID,
167550
+ tab: "general",
167551
+ title: "Scheduled refresh",
167552
+ description: "How often this battery camera is guaranteed to have been read, so a battery level never goes stale unseen.",
167553
+ columns: 1,
167554
+ fields: [{
167555
+ type: "number",
167556
+ key: BATTERY_REFRESH_MINUTES_KEY,
167557
+ label: "Refresh window (min)",
167558
+ description: BATTERY_REFRESH_MINUTES_DESCRIPTION,
167559
+ default: 60,
167560
+ min: 0,
167561
+ max: 240,
167562
+ step: 5
167563
+ }]
167564
+ }];
167565
+ }
166994
167566
  //#endregion
166995
- //#region src/battery-contact-expiry.ts
166996
- function shouldWakeForContact(input) {
166997
- if (input.hubChild) return false;
166998
- if (!input.sleeping) return false;
166999
- const last = input.lastContactAt;
167000
- if (typeof last !== "number" || last <= 0) return false;
167001
- return input.nowMs - last + input.tickMs >= input.unreachableAfterMs;
167567
+ //#region src/battery-snapshot-refresh.ts
167568
+ /** The wrapper may run on another node; a stale frame is hours old, skew is seconds. */
167569
+ var CLOCK_SKEW_TOLERANCE_MS = 6e4;
167570
+ async function refreshSnapshotCacheOnce(input) {
167571
+ const { deviceId, api, logger } = input;
167572
+ const startedAt = input.now();
167573
+ if (await api.getSnapshot({
167574
+ deviceId,
167575
+ force: true
167576
+ }) === null) {
167577
+ logger.warn("battery hourly refresh: the snapshot came back empty", { tags: { deviceId } });
167578
+ return false;
167579
+ }
167580
+ let rows;
167581
+ try {
167582
+ rows = await api.getSnapshotOverview({ deviceIds: [deviceId] });
167583
+ } catch (err) {
167584
+ logger.debug("battery hourly refresh: snapshot overview unreadable — counting the image", {
167585
+ tags: { deviceId },
167586
+ meta: { error: err instanceof Error ? err.message : String(err) }
167587
+ });
167588
+ return true;
167589
+ }
167590
+ const capturedAt = rows.find((row) => row.deviceId === deviceId)?.lastCapturedAt ?? null;
167591
+ if (capturedAt === null || capturedAt < startedAt - CLOCK_SKEW_TOLERANCE_MS) {
167592
+ logger.warn("battery hourly refresh: the snapshot cache did not move (a stale frame was served)", {
167593
+ tags: { deviceId },
167594
+ meta: {
167595
+ capturedAt,
167596
+ startedAt
167597
+ }
167598
+ });
167599
+ return false;
167600
+ }
167601
+ return true;
167002
167602
  }
167003
167603
  //#endregion
167004
167604
  //#region src/log-channels.ts
@@ -171001,7 +171601,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
171001
171601
  * passively checks sleep state, and short-circuits if sleeping. */
171002
171602
  batteryUpdateTimer = null;
171003
171603
  /** Single-flight gate so two interval fires can't overlap. */
171004
- batteryUpdateInFlight = null;
171005
171604
  /** Aux accessory references registered by spawned children
171006
171605
  * (siren / floodlight / pir). The parent's centralised align
171007
171606
  * method iterates these to refresh their slices on a single
@@ -171339,7 +171938,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
171339
171938
  * snapshot also drives the dispatch's known-good baseline.
171340
171939
  */
171341
171940
  async refreshParentSettingsSnapshot(slices, reason = "background") {
171342
- if (this.isBattery && this.sleeping && reason !== "post-write") {
171941
+ if (this.isBattery && this.sleeping && reason !== "post-write" && reason !== "scheduled") {
171343
171942
  this.ctx.logger.info("refreshParentSettingsSnapshot skipped — battery cam is sleeping", { tags: {
171344
171943
  deviceId: this.id,
171345
171944
  reason
@@ -172285,7 +172884,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
172285
172884
  tags: { deviceId: this.id },
172286
172885
  meta: { reason }
172287
172886
  });
172288
- return;
172887
+ return false;
172289
172888
  }
172290
172889
  let api;
172291
172890
  try {
@@ -172295,17 +172894,19 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
172295
172894
  tags: { deviceId: this.id },
172296
172895
  meta: { error: err instanceof Error ? err.message : String(err) }
172297
172896
  });
172298
- return;
172897
+ return false;
172299
172898
  }
172300
172899
  try {
172301
172900
  const info = await api.getBatteryInfo(this.getChannel());
172302
172901
  this.updateBatteryCache(info);
172303
172902
  await this.refreshNetworkLinkFromApi(api, "battery");
172903
+ return true;
172304
172904
  } catch (err) {
172305
172905
  this.ctx.logger.debug("battery refresh failed", {
172306
172906
  tags: { deviceId: this.id },
172307
172907
  meta: { error: err instanceof Error ? err.message : String(err) }
172308
172908
  });
172909
+ return false;
172309
172910
  }
172310
172911
  }
172311
172912
  /**
@@ -173551,6 +174152,18 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
173551
174152
  * proactive gate lets it through because `sleeping` is false here.
173552
174153
  * Mirrors the reference's `updateBatteryAndSnapshot`.
173553
174154
  */
174155
+ /** The scheduled refresh's snapshot step: true only when the cache really moved. */
174156
+ refreshSnapshotCache() {
174157
+ return refreshSnapshotCacheOnce({
174158
+ deviceId: this.id,
174159
+ logger: this.ctx.logger,
174160
+ now: () => Date.now(),
174161
+ api: {
174162
+ getSnapshot: (input) => this.ctx.api.snapshot.getSnapshot.query(input),
174163
+ getSnapshotOverview: (input) => this.ctx.api.snapshot.getSnapshotOverview.query({ deviceIds: [...input.deviceIds] })
174164
+ }
174165
+ });
174166
+ }
173554
174167
  async warmSnapshotOnWake() {
173555
174168
  try {
173556
174169
  await this.ctx.api.snapshot.getSnapshot.query({
@@ -175986,119 +176599,84 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
175986
176599
  this.alignAuxTimer = null;
175987
176600
  }
175988
176601
  }
175989
- /** Run one cycle of the battery-cam periodic refresh. Single-flight
175990
- * via `batteryUpdateInFlight`. Never throws. */
175991
- async runBatteryUpdate() {
175992
- if (this.batteryUpdateInFlight) return this.batteryUpdateInFlight;
175993
- const cycle = (async () => {
176602
+ batteryRefresh = null;
176603
+ getBatteryRefresh() {
176604
+ if (this.batteryRefresh === null) this.batteryRefresh = new BatteryHourlyRefresh({
176605
+ deviceId: this.id,
176606
+ logger: this.ctx.logger,
176607
+ now: () => Date.now(),
176608
+ isAwake: () => !this.sleeping,
176609
+ intervalMs: () => resolveBatteryRefreshIntervalMs(this.config.get("batteryRefreshMinutes")),
176610
+ lastFullRefreshAt: () => this.state.battery.lastFullRefreshAt,
176611
+ wake: async () => {
176612
+ if (!this.canProactivelyWake("scheduled-refresh")) return false;
176613
+ this.markWakeIssued();
176614
+ return this.wakeIfSleeping();
176615
+ },
176616
+ refreshAll: () => this.refreshEverythingNow(),
176617
+ stamp: (at) => {
176618
+ this.state.battery.lastFullRefreshAt = at;
176619
+ }
176620
+ });
176621
+ return this.batteryRefresh;
176622
+ }
176623
+ /**
176624
+ * Every fact and the snapshot, read while the camera is up. SERIALIZED (one
176625
+ * Baichuan round-trip at a time on the single BCUDP stream), and each step
176626
+ * names itself in `failed` instead of throwing, so one dead read neither
176627
+ * hides the others nor stamps the window as done.
176628
+ */
176629
+ async refreshEverythingNow() {
176630
+ const failed = [];
176631
+ const step = async (name, run) => {
175994
176632
  try {
175995
- if (this.isBattery && this.sleeping) {
175996
- if (shouldWakeForContact({
175997
- nowMs: Date.now(),
175998
- lastContactAt: this.state.battery.lastContactAt,
175999
- sleeping: true,
176000
- hubChild: this.isHubChild(),
176001
- unreachableAfterMs: 216e5,
176002
- tickMs: ReolinkCamera.BATTERY_UPDATE_INTERVAL_MS
176003
- })) {
176004
- await this.wakeForContactRefresh();
176005
- return;
176006
- }
176007
- this.ctx.logger.debug("battery-update: cam sleeping — skipping cycle without connecting", { tags: { deviceId: this.id } });
176008
- return;
176009
- }
176010
- let api;
176011
- try {
176012
- api = await this.ensureApi();
176013
- } catch (err) {
176014
- this.ctx.logger.debug("battery-update: ensureApi failed — skipping cycle", {
176015
- tags: { deviceId: this.id },
176016
- meta: { error: err instanceof Error ? err.message : String(err) }
176017
- });
176018
- return;
176019
- }
176020
- let sleeping = false;
176021
- try {
176022
- sleeping = api.getSleepStatus({ channel: this.getChannel() }).state === "sleeping";
176023
- } catch (err) {
176024
- this.ctx.logger.debug("battery-update: getSleepStatus threw — skipping cycle", {
176025
- tags: { deviceId: this.id },
176026
- meta: { error: err instanceof Error ? err.message : String(err) }
176027
- });
176028
- return;
176029
- }
176030
- if (sleeping) {
176031
- this.ctx.logger.debug("battery-update: cam sleeping — skipping periodic refresh", { tags: { deviceId: this.id } });
176032
- return;
176033
- }
176034
- this.ctx.logger.info("battery-update: cam awake — running periodic refresh", {
176633
+ if (await run() === false) failed.push(name);
176634
+ } catch (err) {
176635
+ failed.push(name);
176636
+ this.ctx.logger.debug("battery hourly refresh: step threw", {
176035
176637
  tags: { deviceId: this.id },
176036
176638
  meta: {
176037
- isHubChild: this.isHubChild(),
176038
- auxAccessoryCount: this.auxAccessoryRefs.size
176639
+ step: name,
176640
+ error: err instanceof Error ? err.message : String(err)
176039
176641
  }
176040
176642
  });
176041
- await this.refreshBatteryFromApi("periodic").catch(() => {});
176042
- await this.alignAuxDevicesState("periodic").catch(() => {});
176043
- await this.refreshParentSettingsSnapshot().catch(() => {});
176044
- } finally {
176045
- this.batteryUpdateInFlight = null;
176046
176643
  }
176047
- })();
176048
- this.batteryUpdateInFlight = cycle;
176049
- return cycle;
176644
+ };
176645
+ await step("battery", () => this.refreshBatteryFromApi("demand"));
176646
+ await step("device-info", async () => {
176647
+ await this.populateMetadataFromFirmware(await this.ensureApi());
176648
+ });
176649
+ await step("cap-slices", async () => {
176650
+ await this.warmCapSlicesOnWake({ force: true });
176651
+ });
176652
+ await step("aux-accessories", () => this.alignAuxDevicesState("wake"));
176653
+ await step("settings-snapshot", () => this.refreshParentSettingsSnapshot(void 0, "scheduled"));
176654
+ await step("snapshot", () => this.refreshSnapshotCache());
176655
+ return { failed };
176050
176656
  }
176051
176657
  /**
176052
- * D355: wake a standalone battery camera ONCE because its last passive
176053
- * contact is about to expire, and refresh every slice while it is up.
176054
- * Bounded by `canProactivelyWake` (the shared cooldown) and by the fact
176055
- * that a wake that answers resets the silence. A wake that fails is left
176056
- * alone: the silence then reaches the budget and `deriveBatteryPresence`
176057
- * says `unreachable` — for the first time on evidence.
176658
+ * Run one scheduled-refresh check. Never throws; costs nothing unless a
176659
+ * refresh is due (see {@link decideBatteryRefresh}).
176058
176660
  */
176059
- async wakeForContactRefresh() {
176060
- if (!this.canProactivelyWake("contact-expiry")) return;
176061
- this.markWakeIssued();
176062
- const lastContactAt = this.state.battery.lastContactAt ?? 0;
176063
- const silenceMs = Date.now() - lastContactAt;
176064
- this.ctx.logger.info("battery contact expiring — waking once to refresh every slice (D355)", {
176065
- tags: { deviceId: this.id },
176066
- meta: {
176067
- silenceMs,
176068
- budgetMs: BATTERY_UNREACHABLE_AFTER_MS
176069
- }
176070
- });
176071
- try {
176072
- await this.ensureApi();
176073
- } catch (err) {
176074
- this.ctx.logger.warn("battery contact wake FAILED — silence continues and the camera will read unreachable at the budget", {
176075
- tags: { deviceId: this.id },
176076
- meta: {
176077
- silenceMs,
176078
- error: err instanceof Error ? err.message : String(err)
176079
- }
176080
- });
176081
- return;
176082
- }
176083
- await this.refreshBatteryFromApi("demand").catch(() => {});
176084
- await this.warmCapSlicesOnWake().catch(() => {});
176085
- await this.alignAuxDevicesState("wake").catch(() => {});
176086
- await this.warmSnapshotOnWake().catch(() => {});
176661
+ async runBatteryUpdate() {
176662
+ if (!this.isBattery) return;
176663
+ await this.getBatteryRefresh().tick();
176087
176664
  }
176088
- /** Arm the periodic battery-cam refresh timer. No-op for wired cams
176089
- * (their state is kept fresh by the 10 s aux-align poll and push
176090
- * events). Idempotent. */
176665
+ /** Arm the scheduled-refresh check. No-op for wired cams (their state is
176666
+ * kept fresh by the 10 s aux-align poll and push events). Idempotent. */
176091
176667
  startBatteryUpdatePolling() {
176092
176668
  if (this.batteryUpdateTimer) return;
176093
176669
  if (!this.isBattery) return;
176094
176670
  this.batteryUpdateTimer = setInterval(() => {
176095
176671
  this.runBatteryUpdate().catch(() => {});
176096
- }, ReolinkCamera.BATTERY_UPDATE_INTERVAL_MS);
176672
+ }, BATTERY_REFRESH_TICK_MS);
176097
176673
  if (typeof this.batteryUpdateTimer.unref === "function") this.batteryUpdateTimer.unref();
176098
- this.ctx.logger.info("battery-update: periodic refresh armed", {
176674
+ this.ctx.logger.info("battery-update: scheduled full refresh armed", {
176099
176675
  tags: { deviceId: this.id },
176100
176676
  meta: {
176101
- intervalMs: ReolinkCamera.BATTERY_UPDATE_INTERVAL_MS,
176677
+ tickMs: BATTERY_REFRESH_TICK_MS,
176678
+ intervalMs: resolveBatteryRefreshIntervalMs(this.config.get("batteryRefreshMinutes")),
176679
+ lastFullRefreshAt: this.state.battery.lastFullRefreshAt ?? null,
176102
176680
  isHubChild: this.isHubChild()
176103
176681
  }
176104
176682
  });
@@ -176194,22 +176772,18 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
176194
176772
  meta: { error: err instanceof Error ? err.message : String(err) }
176195
176773
  });
176196
176774
  }
176197
- await this.warmCapSlicesOnWake();
176198
- await this.warmSnapshotOnWake();
176199
- await this.alignAuxDevicesState("wake");
176200
- await this.refreshParentSettingsSnapshot();
176775
+ if (!(this.isBattery ? await this.getBatteryRefresh().noteAwake().catch(() => false) : false)) {
176776
+ await this.warmCapSlicesOnWake();
176777
+ await this.warmSnapshotOnWake();
176778
+ await this.alignAuxDevicesState("wake");
176779
+ await this.refreshParentSettingsSnapshot();
176780
+ }
176201
176781
  await this.refreshClipCatalogOnWake();
176202
176782
  }
176203
176783
  static PING_INTERVAL_MS = 3e4;
176204
176784
  static PING_MAX_FAILURES = 3;
176205
176785
  static EVENT_CHECK_INTERVAL_MS = 6e4;
176206
176786
  static EVENT_STALE_THRESHOLD_MS = 10 * 6e4;
176207
- /** Battery-cam periodic refresh cadence. The timer fires on this
176208
- * interval, passively checks sleep state, and only runs the
176209
- * refresh when the cam is already awake — so it never forces a
176210
- * wake. Mirrors the reference's `batteryUpdateIntervalMinutes`
176211
- * default (60 minutes). */
176212
- static BATTERY_UPDATE_INTERVAL_MS = 60 * 6e4;
176213
176787
  /**
176214
176788
  * Arm the connection-liveness ping watchdog and the event-watchdog.
176215
176789
  * Battery cams skip the ping watchdog — they keep `setIdleDisconnect=true`
@@ -177393,7 +177967,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
177393
177967
  ...buildClipRefreshDeviceSection({
177394
177968
  isHubChild: this.isHubChild(),
177395
177969
  isBattery: this.isBattery
177396
- })
177970
+ }),
177971
+ ...buildBatteryRefreshDeviceSection({ isBattery: this.isBattery })
177397
177972
  ]
177398
177973
  };
177399
177974
  const emailSnap = cache?.emailConfigSnapshot;
@@ -177414,6 +177989,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
177414
177989
  emailScheduleEnabled: emailSnap?.taskEnabled ?? false,
177415
177990
  clipCatalogRefreshMinutes: this.config.get("clipCatalogRefreshMinutes"),
177416
177991
  clipCatalogForcedWake: this.config.get("clipCatalogForcedWake") ?? false,
177992
+ batteryRefreshMinutes: this.config.get("batteryRefreshMinutes"),
177417
177993
  debugGeneral: this.config.get("debugGeneral") ?? false,
177418
177994
  debugSocketLogs: this.config.get("debugSocketLogs") ?? [],
177419
177995
  motionEnabled: this.config.get("motionEnabled") ?? motionSnap.enabled ?? true,