@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.js CHANGED
@@ -11,6 +11,7 @@ let crypto$1 = require("crypto");
11
11
  crypto$1 = require_chunk.__toESM(crypto$1, 1);
12
12
  let child_process = require("child_process");
13
13
  let node_http = require("node:http");
14
+ require("node:zlib");
14
15
  let node_fs_promises = require("node:fs/promises");
15
16
  let node_path = require("node:path");
16
17
  node_path = require_chunk.__toESM(node_path, 1);
@@ -32,10 +33,10 @@ http = require_chunk.__toESM(http, 1);
32
33
  let url = require("url");
33
34
  let node_child_process = require("node:child_process");
34
35
  let node_os = require("node:os");
35
- node_os = require_chunk.__toESM(node_os);
36
+ node_os = require_chunk.__toESM(node_os, 1);
36
37
  let node_fs = require("node:fs");
37
38
  node_fs = require_chunk.__toESM(node_fs, 1);
38
- //#region ../types/dist/event-category-DaIZi00R.mjs
39
+ //#region ../types/dist/event-category-C4hVnRo8.mjs
39
40
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
40
41
  EventCategory["SystemBoot"] = "system.boot";
41
42
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -254,7 +255,17 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
254
255
  EventCategory["RecordingStopped"] = "recording.stopped";
255
256
  EventCategory["RecordingError"] = "recording.error";
256
257
  EventCategory["RecordingHealthDegraded"] = "recording.health.degraded";
258
+ /** Recording enabled but no segment finalized for the NC stall window. */
259
+ EventCategory["RecordingHealthStalled"] = "recording.health.stalled";
260
+ /** Recovery from {@link RecordingHealthStalled}. */
261
+ EventCategory["RecordingHealthOk"] = "recording.health.ok";
257
262
  EventCategory["RecordingStorageCritical"] = "recording.storage.critical";
263
+ /** Free space on a storage location crossed the warn bar. */
264
+ EventCategory["StorageLocationLow"] = "storage.location.low";
265
+ /** Free space at/under the critical bar, or capacity probe failed (`null`). */
266
+ EventCategory["StorageLocationCritical"] = "storage.location.critical";
267
+ /** Recovery from low/critical on a storage location. */
268
+ EventCategory["StorageLocationOk"] = "storage.location.ok";
258
269
  EventCategory["RecordingSegmentWritten"] = "recording.segment.written";
259
270
  EventCategory["RecordingPolicyFallback"] = "recording.policy.fallback";
260
271
  EventCategory["RecordingRetentionCompleted"] = "recording.retention.completed";
@@ -5413,7 +5424,7 @@ var ZodIssueCode = {
5413
5424
  var ZodFirstPartyTypeKind;
5414
5425
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5415
5426
  //#endregion
5416
- //#region ../types/dist/sleep-r3RWkGVx.mjs
5427
+ //#region ../types/dist/sleep-vWTnHq9G.mjs
5417
5428
  /**
5418
5429
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5419
5430
  * window to float samples (D455).
@@ -8018,6 +8029,21 @@ var ProfileRtspEntrySchema = object({
8018
8029
  codec: string().optional(),
8019
8030
  resolution: CamStreamResolutionSchema.optional()
8020
8031
  });
8032
+ /**
8033
+ * The model weight formats a catalog entry can ship. Schema-free on purpose:
8034
+ * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
8035
+ * (the model downloader in `@camstack/system`) needs only the list and must
8036
+ * reach it through `@camstack/types/addon` without building the cap schema
8037
+ * graph (D28).
8038
+ */
8039
+ var MODEL_FORMATS = [
8040
+ "onnx",
8041
+ "coreml",
8042
+ "openvino",
8043
+ "tflite",
8044
+ "pt",
8045
+ "gguf"
8046
+ ];
8021
8047
  object({
8022
8048
  field: string().min(1),
8023
8049
  on: literal("advance")
@@ -8616,7 +8642,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
8616
8642
  return DeviceRole;
8617
8643
  }({});
8618
8644
  //#endregion
8619
- //#region ../types/dist/composition-Bzr7GwgP.mjs
8645
+ //#region ../types/dist/composition-C-WpVYTX.mjs
8620
8646
  /**
8621
8647
  * The ffmpeg build every CamStack node downloads, per platform and arch.
8622
8648
  *
@@ -8668,7 +8694,7 @@ var BASE = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFM
8668
8694
  function portable(target) {
8669
8695
  return `${BASE}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
8670
8696
  }
8671
- portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64");
8697
+ portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64"), `${BASE}${FFMPEG_RELEASE}`;
8672
8698
  /**
8673
8699
  * THE table "which boolean of this device type counts as HIGH", and the
8674
8700
  * pure evaluator of its EDGE.
@@ -9471,6 +9497,16 @@ var CompositionSchema = object({
9471
9497
  seen.add(feature.cap);
9472
9498
  });
9473
9499
  });
9500
+ var NodeColorIdSchema = _enum([
9501
+ "node-a",
9502
+ "node-b",
9503
+ "node-c",
9504
+ "node-d",
9505
+ "node-e",
9506
+ "node-f",
9507
+ "node-g",
9508
+ "node-h"
9509
+ ]);
9474
9510
  new Set([
9475
9511
  "track",
9476
9512
  "face",
@@ -12402,14 +12438,6 @@ var MASK_SUPPLY_STEP_IDS = ["segmentation"];
12402
12438
  ...CROP_SUPPLY_STEP_IDS,
12403
12439
  ...MASK_SUPPLY_STEP_IDS
12404
12440
  ];
12405
- var MODEL_FORMATS = [
12406
- "onnx",
12407
- "coreml",
12408
- "openvino",
12409
- "tflite",
12410
- "pt",
12411
- "gguf"
12412
- ];
12413
12441
  /**
12414
12442
  * Multi-file format payload.
12415
12443
  *
@@ -14233,7 +14261,14 @@ var BrokerProviderInfoSchema = object({
14233
14261
  /** Broker kinds this provider can create, with a display label. */
14234
14262
  kinds: array(object({
14235
14263
  kind: string(),
14236
- label: string()
14264
+ label: string(),
14265
+ /**
14266
+ * Relative hub path to start an interactive account link (OAuth popup).
14267
+ * May include `{{brokerId}}`; admin-ui substitutes and appends `?popup=1`.
14268
+ */
14269
+ interactiveConnectPath: string().min(1).optional(),
14270
+ /** Relative hub path to disconnect the linked account (same popup pattern). */
14271
+ interactiveDisconnectPath: string().min(1).optional()
14237
14272
  }))
14238
14273
  });
14239
14274
  var ListInputSchema = object({
@@ -19184,14 +19219,14 @@ var LlmProfileSchema = object({
19184
19219
  /** ConfigUISchema tree passed through untyped on the wire (the
19185
19220
  * notification-output `ConfigSchemaPassthrough` precedent at
19186
19221
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
19187
- var ConfigSchemaPassthrough$1 = unknown();
19222
+ var ConfigSchemaPassthrough$3 = unknown();
19188
19223
  var LlmProfileKindDescriptorSchema = object({
19189
19224
  kind: LlmProfileKindSchema,
19190
19225
  label: string(),
19191
19226
  icon: string(),
19192
19227
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
19193
19228
  addonId: string(),
19194
- configSchema: ConfigSchemaPassthrough$1
19229
+ configSchema: ConfigSchemaPassthrough$3
19195
19230
  });
19196
19231
  var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
19197
19232
  var LlmDefaultSchema = object({
@@ -20333,7 +20368,7 @@ var TargetKindCapsSchema = object({
20333
20368
  * the union is large and not meant for runtime validation here; the exported
20334
20369
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
20335
20370
  */
20336
- var ConfigSchemaPassthrough = unknown();
20371
+ var ConfigSchemaPassthrough$2 = unknown();
20337
20372
  var TargetKindSchema = object({
20338
20373
  kind: string(),
20339
20374
  label: string(),
@@ -20370,7 +20405,7 @@ var TargetKindSchema = object({
20370
20405
  * its raster path, which is the safe default for an unknown image.
20371
20406
  */
20372
20407
  iconMediaType: string().optional(),
20373
- configSchema: ConfigSchemaPassthrough,
20408
+ configSchema: ConfigSchemaPassthrough$2,
20374
20409
  /**
20375
20410
  * The config keys that decide WHERE this kind's secrets are sent (server,
20376
20411
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -21327,6 +21362,12 @@ var NcSystemEventKindSchema = _enum([
21327
21362
  "node-updating",
21328
21363
  "node-inference-unavailable",
21329
21364
  "detection-blind",
21365
+ "recording-stalled",
21366
+ "recording-healthy",
21367
+ "recording-degraded",
21368
+ "storage-location-low",
21369
+ "storage-location-critical",
21370
+ "storage-location-ok",
21330
21371
  "addon-crash-loop",
21331
21372
  "addon-update-available",
21332
21373
  "server-update-available",
@@ -23707,6 +23748,61 @@ var OauthIntegrationDescriptorSchema = object({
23707
23748
  });
23708
23749
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
23709
23750
  /**
23751
+ * `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
23752
+ *
23753
+ * One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
23754
+ * ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
23755
+ * Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
23756
+ * immutable after create.
23757
+ */
23758
+ var ConfigSchemaPassthrough$1 = unknown();
23759
+ var OidcConnectionSchema = object({
23760
+ id: string().min(1),
23761
+ displayName: string(),
23762
+ icon: string(),
23763
+ issuerUrl: string(),
23764
+ clientId: string(),
23765
+ /** Client id + secret present — never the secret value. */
23766
+ configured: boolean(),
23767
+ /**
23768
+ * Values for the connection form schema. `clientSecret` is a keychain
23769
+ * {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
23770
+ */
23771
+ formValues: record(string(), unknown()).optional()
23772
+ });
23773
+ /**
23774
+ * Patch one connection. Omitted or empty `clientSecret` keeps the stored
23775
+ * secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
23776
+ */
23777
+ var OidcConnectionUpdateInputSchema = object({
23778
+ id: string().min(1),
23779
+ displayName: string().min(1),
23780
+ icon: string().optional(),
23781
+ issuerUrl: string().optional(),
23782
+ clientId: string().optional(),
23783
+ clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
23784
+ redirectUri: string().optional(),
23785
+ scopes: string().optional(),
23786
+ usernameClaim: _enum([
23787
+ "preferred_username",
23788
+ "email",
23789
+ "sub"
23790
+ ]).optional()
23791
+ });
23792
+ method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
23793
+ id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
23794
+ displayName: string().min(1)
23795
+ }), object({ id: string() }), {
23796
+ kind: "mutation",
23797
+ auth: "admin"
23798
+ }), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
23799
+ kind: "mutation",
23800
+ auth: "admin"
23801
+ }), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
23802
+ kind: "mutation",
23803
+ auth: "admin"
23804
+ });
23805
+ /**
23710
23806
  * What started a camera's onboard motion (D672). `camera` is the camera's own
23711
23807
  * firmware; `sensor` is a device the operator ATTACHED to it. Absent on the
23712
23808
  * wire means the camera: only the camera-sensors bridge ever says `sensor`.
@@ -25063,24 +25159,69 @@ var RetrainFrameListSchema = object({
25063
25159
  /** What the page pre-selects — the native key frame when one survives. */
25064
25160
  autoPickMediaId: number().int().optional()
25065
25161
  });
25162
+ /** Exclusive partition of a corpus copy's annotation progress (design §4.2). */
25163
+ var RetrainFrameWorkStateSchema = _enum([
25164
+ "unannotated",
25165
+ "annotated",
25166
+ "complete"
25167
+ ]);
25168
+ /**
25169
+ * Filters for paginated corpus list, KPI, and matching export (design §4.4).
25170
+ * Every narrowing field is ANDed; omitted fields impose no constraint.
25171
+ */
25172
+ var RetrainCorpusFilterSchema = object({
25173
+ workState: RetrainFrameWorkStateSchema.optional(),
25174
+ deviceIds: array(number()).max(500).optional(),
25175
+ /** When set, only frames in this named collection. */
25176
+ collectionId: string().optional(),
25177
+ /** Frames that carry at least one saved annotation in one of these classes. */
25178
+ classes: array(RetrainMacroClassSchema).optional(),
25179
+ page: number().int().positive().default(1),
25180
+ pageSize: number().int().positive().max(100).default(50)
25181
+ });
25182
+ var RetrainCorpusPageSchema = object({
25183
+ frames: array(RetrainFrameSchema.extend({ workState: RetrainFrameWorkStateSchema })).readonly(),
25184
+ total: number().int().nonnegative(),
25185
+ page: number().int().positive(),
25186
+ pageSize: number().int().positive()
25187
+ });
25188
+ var RetrainCorpusKpiCameraRowSchema = object({
25189
+ deviceId: number(),
25190
+ unannotated: number().int().nonnegative(),
25191
+ annotated: number().int().nonnegative(),
25192
+ complete: number().int().nonnegative(),
25193
+ /** This camera's `complete` count divided by corpus `total`, in `[0,1]`.
25194
+ * `null` when the corpus is empty (no ratio). */
25195
+ pctOfCorpus: number().min(0).max(1).nullable()
25196
+ });
25197
+ var RetrainCorpusKpiSchema = object({
25198
+ total: number().int().nonnegative(),
25199
+ unannotated: number().int().nonnegative(),
25200
+ annotated: number().int().nonnegative(),
25201
+ complete: number().int().nonnegative(),
25202
+ cameras: array(RetrainCorpusKpiCameraRowSchema).readonly()
25203
+ });
25066
25204
  /**
25067
25205
  * Which frames an annotated export carries. Every field narrows; an empty
25068
- * filter is every annotated copy. A dataset VERSION is a frozen `frameIds`
25069
- * list owned by whoever trains (the training orchestrator), so `frameIds` is
25070
- * the primitive a version export uses; `batchId` re-selects what an earlier
25071
- * export carried (`exportedInBatch`).
25206
+ * filter is every annotated copy. `frameIds` is the primitive a job or
25207
+ * download uses; `batchId` re-selects what an earlier export carried
25208
+ * (`exportedInBatch`). Frozen dataset versions were removed — live selection
25209
+ * resolves to `frameIds` at plan/start.
25072
25210
  */
25073
25211
  var RetrainExportFilterSchema = object({
25074
25212
  deviceIds: array(number()).optional(),
25075
25213
  frameIds: array(string()).max(2e4).optional(),
25076
25214
  /** Only frames in at least one of these named collections. */
25077
25215
  collectionIds: array(string()).max(200).optional(),
25078
- /** Exactly the frames of this dataset version (and its completeOnly). An
25079
- * unknown id selects nothing. */
25080
- datasetVersionId: string().optional(),
25216
+ /** Single collection (Frames filter bar). ANDed with `collectionIds` when both set. */
25217
+ collectionId: string().optional(),
25081
25218
  batchId: string().optional(),
25082
25219
  /** Only frames the operator marked complete (all subjects boxed). */
25083
- completeOnly: boolean().optional()
25220
+ completeOnly: boolean().optional(),
25221
+ /** Exclusive work-state partition (`workStateOf`); same as the corpus list. */
25222
+ workState: RetrainFrameWorkStateSchema.optional(),
25223
+ /** Frames with at least one saved annotation in one of these classes. */
25224
+ classes: array(RetrainMacroClassSchema).optional()
25084
25225
  });
25085
25226
  /**
25086
25227
  * `frames`: the full frame copies, boxes in `annotations.json`.
@@ -25280,8 +25421,7 @@ var RetrainDatasetStatsSchema = object({
25280
25421
  completeFrames: number().int()
25281
25422
  })).readonly()
25282
25423
  });
25283
- /** An immutable dataset version, without its frame list. */
25284
- var RetrainDatasetVersionSchema = object({
25424
+ object({
25285
25425
  id: string(),
25286
25426
  name: string(),
25287
25427
  createdAt: number(),
@@ -25289,22 +25429,33 @@ var RetrainDatasetVersionSchema = object({
25289
25429
  layout: RetrainExportLayoutSchema,
25290
25430
  completeOnly: boolean(),
25291
25431
  frameCount: number().int(),
25292
- /** sha256 of what the frames taught when the version was frozen. */
25293
25432
  annotationHash: string(),
25294
- /** The export manifest schema the version was cut under. */
25295
25433
  manifestSchema: string(),
25296
25434
  stats: RetrainDatasetStatsSchema
25297
25435
  });
25298
- var CreateRetrainDatasetVersionResultSchema = discriminatedUnion("kind", [object({
25299
- kind: literal("created"),
25300
- version: RetrainDatasetVersionSchema
25436
+ /**
25437
+ * Live training corpus criteria — same shape as `TrainingCorpusSelection` on
25438
+ * the training cap. Resolved here so plan/start and the annotated export share
25439
+ * one definition of "the frames" (`buildRetrainExportPlan`).
25440
+ */
25441
+ var RetrainTrainingCorpusSelectionSchema = object({
25442
+ collectionIds: array(string()).min(1),
25443
+ workState: _enum([
25444
+ "complete",
25445
+ "annotated_or_complete",
25446
+ "any_annotated"
25447
+ ]).default("complete")
25448
+ });
25449
+ var RetrainTrainingCorpusResolveResultSchema = discriminatedUnion("kind", [object({
25450
+ kind: literal("resolved"),
25451
+ frameIds: array(string()).max(2e4).readonly(),
25452
+ stats: RetrainDatasetStatsSchema,
25453
+ annotationHash: string(),
25454
+ layout: RetrainExportLayoutSchema
25301
25455
  }), object({
25302
25456
  kind: literal("refused"),
25303
- reason: _enum([
25304
- "empty-name",
25305
- "duplicate-name",
25306
- "no-frames"
25307
- ])
25457
+ reason: _enum(["unknown-collection", "no-frames"]),
25458
+ detail: string()
25308
25459
  })]);
25309
25460
  /** The outcome of a lifecycle move owned by the retrain page. */
25310
25461
  var RetrainTransitionResultSchema = object({
@@ -26011,6 +26162,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
26011
26162
  }), array(RetrainTrackSchema).readonly(), {
26012
26163
  kind: "query",
26013
26164
  auth: "admin"
26165
+ }), method(RetrainCorpusFilterSchema, RetrainCorpusPageSchema, {
26166
+ kind: "query",
26167
+ auth: "admin"
26168
+ }), method(object({}), RetrainCorpusKpiSchema, {
26169
+ kind: "query",
26170
+ auth: "admin"
26014
26171
  }), method(object({ trackId: string() }), RetrainFrameListSchema, {
26015
26172
  kind: "query",
26016
26173
  auth: "admin"
@@ -26106,36 +26263,6 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
26106
26263
  kind: "mutation",
26107
26264
  auth: "admin",
26108
26265
  timeoutMs: 12e4
26109
- }), method(object({
26110
- selection: RetrainDatasetSelectionSchema,
26111
- completeOnly: boolean().optional()
26112
- }), RetrainDatasetStatsSchema, {
26113
- kind: "query",
26114
- auth: "admin"
26115
- }), method(object({
26116
- name: string().max(120),
26117
- selection: RetrainDatasetSelectionSchema,
26118
- layout: RetrainExportLayoutSchema,
26119
- completeOnly: boolean()
26120
- }), CreateRetrainDatasetVersionResultSchema, {
26121
- kind: "mutation",
26122
- auth: "admin",
26123
- timeoutMs: 12e4
26124
- }), method(object({}), array(RetrainDatasetVersionSchema).readonly(), {
26125
- kind: "query",
26126
- auth: "admin"
26127
- }), method(object({ id: string() }), object({ deleted: boolean() }), {
26128
- kind: "mutation",
26129
- auth: "admin"
26130
- }), method(object({ id: string() }), object({
26131
- found: boolean(),
26132
- annotationHash: string(),
26133
- matches: boolean(),
26134
- missingFrames: number().int()
26135
- }), {
26136
- kind: "query",
26137
- auth: "admin",
26138
- timeoutMs: 12e4
26139
26266
  }), method(object({}), array(RetrainAssistProfileSchema).readonly(), {
26140
26267
  kind: "query",
26141
26268
  auth: "admin"
@@ -26162,6 +26289,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
26162
26289
  }), method(object({}), array(RetrainCollectionSchema).readonly(), {
26163
26290
  kind: "query",
26164
26291
  auth: "admin"
26292
+ }), method(RetrainTrainingCorpusSelectionSchema, RetrainTrainingCorpusResolveResultSchema, {
26293
+ kind: "query",
26294
+ auth: "admin",
26295
+ timeoutMs: 6e4
26165
26296
  }), method(object({
26166
26297
  id: string(),
26167
26298
  name: string().max(120)
@@ -26778,11 +26909,14 @@ var HardwareClassIdSchema = _enum([
26778
26909
  "intel-n-series",
26779
26910
  "apple-m-base",
26780
26911
  "apple-m-pro",
26912
+ "amd-phoenix",
26781
26913
  "unknown"
26782
26914
  ]);
26783
26915
  var AcceleratorKindSchema = _enum([
26784
26916
  "intel-npu",
26785
26917
  "intel-igpu",
26918
+ "amd-igpu",
26919
+ "amd-npu",
26786
26920
  "x86-cpu",
26787
26921
  "arm-cpu",
26788
26922
  "apple-ane",
@@ -28712,6 +28846,8 @@ var AgentPipelineSettingsSchema = object({
28712
28846
  * address is wrong (multi-homed host, NAT, custom interface).
28713
28847
  */
28714
28848
  reachableHost: string().optional(),
28849
+ /** Hub-persisted UI colour (`--color-node-*` theme tokens). */
28850
+ color: NodeColorIdSchema.optional(),
28715
28851
  /**
28716
28852
  * Multi-device inference opt-in (Phase 4). Per-node map deviceKey → {enabled,
28717
28853
  * weight, steps}. Absent / all-disabled ⇒ the node's single default
@@ -29261,8 +29397,9 @@ method(object({
29261
29397
  auth: "admin"
29262
29398
  }), method(object({
29263
29399
  agentNodeId: string(),
29264
- reachableHost: string().nullable()
29265
- }), object({ success: literal(true) }), {
29400
+ reachableHost: string().nullable().optional(),
29401
+ color: NodeColorIdSchema.nullable().optional()
29402
+ }).refine((v) => v.reachableHost !== void 0 || v.color !== void 0, { message: "reachableHost or color required" }), object({ success: literal(true) }), {
29266
29403
  kind: "mutation",
29267
29404
  auth: "admin"
29268
29405
  }), method(object({
@@ -32860,7 +32997,7 @@ var CustomActionInputSchema = object({
32860
32997
  action: string().min(1),
32861
32998
  input: unknown()
32862
32999
  });
32863
- method(_void(), array(AddonListItemSchema).readonly()), method(object({
33000
+ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSchema).readonly()), method(object({
32864
33001
  addonId: string(),
32865
33002
  limit: number().min(1).max(500).default(100),
32866
33003
  level: LogLevelSchema$2.optional()
@@ -33330,6 +33467,17 @@ var BatteryStatusSchema = object({
33330
33467
  */
33331
33468
  lastContactAt: number().optional(),
33332
33469
  /**
33470
+ * Ms epoch of the last COMPLETE scheduled refresh — every fact the provider
33471
+ * holds for the device (level, charging, link, device info, state slices) and
33472
+ * its cached snapshot, read together. Absent = never since this slice was
33473
+ * born. Unlike `lastUpdated` (any write, even a push of one field) this is
33474
+ * the staleness an operator can trust: the provider owns the device fact and
33475
+ * so owns the window on its age (D224, D847). Not a clock in the
33476
+ * runtime-state-durability sense: it moves once per window and must survive
33477
+ * a restart, or every restart would owe the camera a wake.
33478
+ */
33479
+ lastFullRefreshAt: number().optional(),
33480
+ /**
33333
33481
  * True when the source is a BINARY low-battery indicator (HA
33334
33482
  * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
33335
33483
  * charge level — `percentage` is then a coarse stand-in (100 = normal,
@@ -38407,6 +38555,7 @@ var OsdNamedTemplateSchema = object({
38407
38555
  });
38408
38556
  /** Where a slot's value comes from. */
38409
38557
  var OsdSourceSchema = discriminatedUnion("kind", [
38558
+ object({ kind: literal("none") }),
38410
38559
  object({
38411
38560
  kind: literal("static"),
38412
38561
  text: string().max(64)
@@ -38533,6 +38682,13 @@ method(object({ deviceId: number().int() }), object({
38533
38682
  }), object({ success: literal(true) }), {
38534
38683
  kind: "mutation",
38535
38684
  auth: "admin"
38685
+ }), method(object({
38686
+ deviceId: number().int(),
38687
+ slotId: string().min(1),
38688
+ enabled: boolean()
38689
+ }), object({ success: literal(true) }), {
38690
+ kind: "mutation",
38691
+ auth: "admin"
38536
38692
  }), method(object({
38537
38693
  sourceDeviceId: number().int(),
38538
38694
  targetDeviceId: number().int()
@@ -38964,7 +39120,15 @@ var GpuInfoSchema = object({
38964
39120
  name: string(),
38965
39121
  memoryMB: number().optional()
38966
39122
  });
38967
- var NpuInfoSchema = object({ type: _enum(["apple-ane", "intel-npu"]) });
39123
+ var NpuInfoSchema = object({
39124
+ type: _enum([
39125
+ "apple-ane",
39126
+ "intel-npu",
39127
+ "amd-xdna"
39128
+ ]),
39129
+ name: string().optional(),
39130
+ generation: _enum(["xdna1", "xdna2"]).optional()
39131
+ });
38968
39132
  var CoralInfoSchema = object({
38969
39133
  type: literal("coral-edgetpu"),
38970
39134
  bus: string().optional()
@@ -44211,9 +44375,11 @@ var zoneRulesCapability = {
44211
44375
  * once is what makes a second provider (RunPod, a local NVIDIA node) a new
44212
44376
  * manifest entry and nothing else.
44213
44377
  *
44214
- * Collection, routed by the `addonId` in every single-row input (the `llm` /
44215
- * `notification-output` posture); `describe` concat-fans across providers.
44216
- * Every method is `admin`: these spend money.
44378
+ * Collection of provider **types** (addons). Each addon owns zero or more
44379
+ * **entries** (credentials + defaults). Concat-fans: `describe`,
44380
+ * `describeTypes`, `listEntries`, `listJobs`. Single-row work is keyed by
44381
+ * `entryId`; callers also pass the owning `addonId` as the collection
44382
+ * selector (broker posture). Every method is `admin`: these spend money.
44217
44383
  *
44218
44384
  * Every cloud object a provider creates is tagged with the orchestrator's
44219
44385
  * `jobKey`, so a retried submit finds the job it already started instead of
@@ -44237,8 +44403,8 @@ var TrainingRegionOptionSchema = object({
44237
44403
  priceMultiplier: number().positive()
44238
44404
  });
44239
44405
  /**
44240
- * The provider's per-account settings, as the orchestrator reads them. They
44241
- * live in the PROVIDER addon's settings (operator decision 2026-10-08: they
44406
+ * The provider's per-entry settings, as the orchestrator reads them. They
44407
+ * live on the PROVIDER addon's entry (operator decision 2026-10-08: they
44242
44408
  * depend on the user's account), never hard-coded in the orchestrator.
44243
44409
  */
44244
44410
  var TrainingProviderDefaultsSchema = object({
@@ -44249,9 +44415,44 @@ var TrainingProviderDefaultsSchema = object({
44249
44415
  /** Hours a FAILED job's cloud dataset is kept for debugging; 0 = delete at once. */
44250
44416
  failedDatasetGraceHours: number().nonnegative()
44251
44417
  });
44418
+ /**
44419
+ * Entry form schema for a provider type — same seam as notification
44420
+ * `TargetKind.configSchema` (`z.unknown()` at the wire; callers tighten to
44421
+ * {@link ConfigUISchema}). Drives `ConfigFormBuilder` on Training → Providers.
44422
+ */
44423
+ var ConfigSchemaPassthrough = unknown();
44424
+ /** One creatable provider type, for the Add-entry dialog. */
44425
+ var TrainingProviderTypeSchema = object({
44426
+ typeId: string(),
44427
+ label: string(),
44428
+ /** Path under the addon package (e.g. `assets/training-modal/icon.svg`). */
44429
+ iconAsset: string(),
44430
+ /** Owning `training-provider` addon — collection selector for createEntry. */
44431
+ addonId: string(),
44432
+ /**
44433
+ * Fields for one entry of this type (credentials + defaults). Absent on a
44434
+ * type that only needs a name. Password fields are keychain-backed on the
44435
+ * provider addon (`secretStatePaths`), not D634 plaintext.
44436
+ */
44437
+ configSchema: ConfigSchemaPassthrough.optional()
44438
+ });
44439
+ /** One configured account / workspace under a provider type. */
44440
+ var TrainingProviderEntrySchema = object({
44441
+ entryId: string(),
44442
+ typeId: string(),
44443
+ name: string(),
44444
+ addonId: string(),
44445
+ /** Credentials are present — never the values. */
44446
+ configured: boolean()
44447
+ });
44252
44448
  var TrainingProviderDescriptorSchema = object({
44253
44449
  addonId: string(),
44254
- /** `modal`, `runpod`, `fake`, … */
44450
+ entryId: string(),
44451
+ typeId: string(),
44452
+ /** Operator-chosen entry name. */
44453
+ name: string(),
44454
+ iconAsset: string(),
44455
+ /** `modal`, `runpod`, `fake`, … — same as typeId for current providers. */
44255
44456
  kind: string(),
44256
44457
  label: string(),
44257
44458
  gpuTypes: array(TrainingGpuOptionSchema).readonly(),
@@ -44263,7 +44464,30 @@ var TrainingProviderDescriptorSchema = object({
44263
44464
  dataResidencyNote: string(),
44264
44465
  /** Credentials are present — never the values. */
44265
44466
  configured: boolean(),
44266
- defaults: TrainingProviderDefaultsSchema
44467
+ defaults: TrainingProviderDefaultsSchema,
44468
+ /**
44469
+ * Values for {@link TrainingProviderTypeSchema}'s `configSchema` — password
44470
+ * fields are keychain {@link SecretFieldView} or `null` / `''` (create),
44471
+ * never plaintext. Same hydrate contract as settings forms (D789).
44472
+ */
44473
+ formValues: record(string(), unknown()).optional()
44474
+ });
44475
+ /**
44476
+ * Patch one entry's name, credentials and defaults. Pin with owning `{ addonId }`.
44477
+ * An omitted or empty `tokenSecret` means KEEP the stored secret (D787).
44478
+ * `{ $secretClear: true }` clears it.
44479
+ */
44480
+ var TrainingProviderUpdateEntryInputSchema = object({
44481
+ entryId: string().min(1),
44482
+ name: string().min(1),
44483
+ tokenId: string().optional(),
44484
+ tokenSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
44485
+ environment: string().optional(),
44486
+ gpuType: string().optional(),
44487
+ region: string().optional(),
44488
+ costCapUsd: number().nonnegative().optional(),
44489
+ maxRuntimeSec: number().int().positive().optional(),
44490
+ failedDatasetGraceHours: number().nonnegative().optional()
44267
44491
  });
44268
44492
  var TrainingCredentialCheckSchema = object({
44269
44493
  ok: boolean(),
@@ -44315,20 +44539,33 @@ var TrainerSourceRefSchema = object({
44315
44539
  /** A commit sha (never a branch) — the job must be reproducible. */
44316
44540
  revision: string().regex(/^[0-9a-f]{7,64}$/)
44317
44541
  });
44542
+ /** Cloud job identity — route with the owning addon's `addonId` selector. */
44318
44543
  var JobRef = {
44319
- addonId: string(),
44544
+ entryId: string().min(1),
44320
44545
  jobKey: string().uuid()
44321
44546
  };
44322
44547
  var TrainingLogLineSchema = object({
44323
44548
  ts: number(),
44324
44549
  text: string()
44325
44550
  });
44326
- method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "admin" }), method(object({ addonId: string() }), TrainingCredentialCheckSchema, {
44551
+ method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderTypeSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderEntrySchema).readonly(), { auth: "admin" }), method(object({
44552
+ typeId: string().min(1),
44553
+ name: string().min(1)
44554
+ }), object({ entryId: string() }), {
44555
+ kind: "mutation",
44556
+ auth: "admin"
44557
+ }), method(object({ entryId: string().min(1) }), object({ ok: literal(true) }), {
44558
+ kind: "mutation",
44559
+ auth: "admin"
44560
+ }), method(TrainingProviderUpdateEntryInputSchema, object({ ok: literal(true) }), {
44561
+ kind: "mutation",
44562
+ auth: "admin"
44563
+ }), method(object({ entryId: string().min(1) }), TrainingCredentialCheckSchema, {
44327
44564
  kind: "mutation",
44328
44565
  auth: "admin",
44329
44566
  timeoutMs: 3e4
44330
44567
  }), method(object({
44331
- addonId: string(),
44568
+ entryId: string().min(1),
44332
44569
  gpuType: string(),
44333
44570
  region: string(),
44334
44571
  datasetBytes: number().int().nonnegative(),
@@ -44394,7 +44631,7 @@ method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "
44394
44631
  kind: "mutation",
44395
44632
  auth: "admin",
44396
44633
  timeoutMs: 5 * 6e4
44397
- }), method(object({ addonId: string() }), array(ProviderJobStatusSchema).readonly(), {
44634
+ }), method(object({ entryId: string().min(1).optional() }), array(ProviderJobStatusSchema).readonly(), {
44398
44635
  auth: "admin",
44399
44636
  timeoutMs: 2 * 6e4
44400
44637
  });
@@ -44404,20 +44641,31 @@ method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "
44404
44641
  *
44405
44642
  * It owns what every provider must agree on, written once:
44406
44643
  *
44407
- * - **Dataset versions.** Taken over from pipeline-analytics (D796): the rows
44408
- * live here (D808); pipeline-analytics still CUTS a version (it owns the corpus and
44409
- * the export plan) and reaches this store through the five row methods
44410
- * below, so the Retrain page and the export did not change.
44411
44644
  * - **Jobs.** A persisted state machine, a reconcile loop against the
44412
44645
  * provider, a cost guard, a daily orphan sweep and the retention rules.
44413
44646
  * - **The return flow.** Verified artefacts → Model Studio
44414
44647
  * (`custom-model-registry.importModelBundle`) → canary on cameras →
44415
44648
  * rollback. Nothing is ever registered automatically.
44416
44649
  *
44650
+ * Plan/start takes a live {@link TrainingCorpusSelection} (collections +
44651
+ * work-state rule); frame ids are resolved via pipeline-analytics at that
44652
+ * moment and snapshotted on the job. No frozen dataset version.
44653
+ *
44417
44654
  * Every method is `admin`.
44418
44655
  */
44419
- /** A version WITH its frame list — what the export and a job read. */
44420
- var TrainingDatasetVersionRowSchema = RetrainDatasetVersionSchema.omit({ frameCount: true }).extend({ frameIds: array(string()).max(2e4).readonly() });
44656
+ /**
44657
+ * Live corpus criteria for plan/start (design §5.2). Default is complete-only.
44658
+ * Frame ids are resolved at plan/start — never a frozen version id.
44659
+ */
44660
+ var TrainingCorpusSelectionSchema = object({
44661
+ collectionIds: array(string()).min(1),
44662
+ /** Default complete-only for v1 */
44663
+ workState: _enum([
44664
+ "complete",
44665
+ "annotated_or_complete",
44666
+ "any_annotated"
44667
+ ]).default("complete")
44668
+ });
44421
44669
  /** A base model a detector job can fine-tune, and the baseline it is judged against. */
44422
44670
  var TrainingBaseModelSchema = object({
44423
44671
  modelId: string(),
@@ -44435,9 +44683,13 @@ var TrainingPresetSchema = _enum([
44435
44683
  "thorough"
44436
44684
  ]);
44437
44685
  var TrainingJobRequestSchema = object({
44438
- datasetVersionId: string().min(1),
44439
- /** The `training-provider` addon that runs it. */
44440
- addonId: string().min(1),
44686
+ /** Live collection + work-state criteria; resolved at plan/start. */
44687
+ selection: TrainingCorpusSelectionSchema,
44688
+ /**
44689
+ * The provider **entry** that runs it (credentials + defaults).
44690
+ * Resolve to an owning addon via `training-provider.describe` / `listEntries`.
44691
+ */
44692
+ entryId: string().min(1),
44441
44693
  gpuType: string().min(1),
44442
44694
  region: string(),
44443
44695
  baseModelId: string().min(1),
@@ -44496,6 +44748,7 @@ var TrainingJobPlanSchema = object({
44496
44748
  * result lives after the operator acted on it.
44497
44749
  */
44498
44750
  var TrainingJobStateSchema = _enum([
44751
+ "queued",
44499
44752
  "uploading",
44500
44753
  "submitted",
44501
44754
  "starting",
@@ -44549,6 +44802,11 @@ var TrainingJobSchema = object({
44549
44802
  createdAt: number(),
44550
44803
  updatedAt: number(),
44551
44804
  state: TrainingJobStateSchema,
44805
+ /**
44806
+ * Shared by every job enqueued together via `startJobBatch`.
44807
+ * `null` for a lone `startJob`. The engine advances queued siblings by this id.
44808
+ */
44809
+ batchId: string().nullable().default(null),
44552
44810
  stage: TrainingStageSchema.nullable(),
44553
44811
  failure: object({
44554
44812
  reason: TrainingFailureReasonSchema,
@@ -44560,6 +44818,12 @@ var TrainingJobSchema = object({
44560
44818
  maxRuntimeSec: number().int().positive(),
44561
44819
  trainer: TrainerSourceRefSchema,
44562
44820
  datasetLayout: RetrainExportLayoutSchema,
44821
+ /** Frame count snapshotted when the job started (audit; equals `frameIds.length`). */
44822
+ frameCount: number().int().nonnegative(),
44823
+ /** Resolved frame ids at start — the archive ships exactly these. */
44824
+ frameIds: array(string()).max(2e4).readonly(),
44825
+ /** Hash of what those frames taught at start (trainer `dataset.annotationHash`). */
44826
+ annotationHash: string(),
44563
44827
  dataset: object({
44564
44828
  bytes: number().int(),
44565
44829
  sha256: string(),
@@ -44601,13 +44865,7 @@ var TrainingJobSchema = object({
44601
44865
  canary: TrainingCanarySchema.nullable()
44602
44866
  });
44603
44867
  var IdInput = object({ id: string().min(1) });
44604
- method(TrainingDatasetVersionRowSchema, object({ stored: boolean() }), {
44605
- kind: "mutation",
44606
- auth: "admin"
44607
- }), 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() }), {
44608
- kind: "mutation",
44609
- auth: "admin"
44610
- }), method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" }), method(TrainingJobRequestSchema, TrainingJobPlanSchema, {
44868
+ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" }), method(TrainingJobRequestSchema, TrainingJobPlanSchema, {
44611
44869
  auth: "admin",
44612
44870
  timeoutMs: 6e4
44613
44871
  }), method(TrainingJobRequestSchema.extend({
@@ -44626,6 +44884,24 @@ method(TrainingDatasetVersionRowSchema, object({ stored: boolean() }), {
44626
44884
  kind: "mutation",
44627
44885
  auth: "admin",
44628
44886
  timeoutMs: 12e4
44887
+ }), method(TrainingJobRequestSchema.omit({ baseModelId: true }).extend({
44888
+ baseModelIds: array(string().min(1)).min(1),
44889
+ name: string().min(1).max(120),
44890
+ maxCostUsd: number().positive(),
44891
+ maxRuntimeSec: number().int().positive(),
44892
+ idempotencyKey: string().min(8).max(80)
44893
+ }), discriminatedUnion("kind", [object({
44894
+ kind: literal("started"),
44895
+ batchId: string(),
44896
+ jobs: array(TrainingJobSchema).min(1).readonly()
44897
+ }), object({
44898
+ kind: literal("refused"),
44899
+ reason: TrainingRefusalReasonSchema,
44900
+ detail: string()
44901
+ })]), {
44902
+ kind: "mutation",
44903
+ auth: "admin",
44904
+ timeoutMs: 12e4
44629
44905
  }), method(object({}), array(TrainingJobSchema).readonly(), { auth: "admin" }), method(IdInput, TrainingJobSchema.nullable(), { auth: "admin" }), method(IdInput, TrainingJobSchema, {
44630
44906
  kind: "mutation",
44631
44907
  auth: "admin",
@@ -46236,17 +46512,6 @@ var BaseDeviceProvider = class extends BaseAddon {
46236
46512
  return toDeviceSummary(device, this.addonId);
46237
46513
  }
46238
46514
  };
46239
- /**
46240
- * Default silence budget before a sleeping battery camera is called gone.
46241
- *
46242
- * Sized off the mechanism that produces contact, not off taste: a battery cam
46243
- * on this deployment surfaces a firmware push (battery level, sleep/wake, or a
46244
- * motion email) on the order of hours even with no visitors, and the snapshot
46245
- * wrapper's own battery window is 1 hour. Six hours is therefore several
46246
- * missed opportunities, not one — so a single quiet night does not raise a
46247
- * fault, and a genuinely flat camera is named well inside a day.
46248
- */
46249
- var BATTERY_UNREACHABLE_AFTER_MS = 360 * 6e4;
46250
46515
  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;
46251
46516
  new Set(Object.values(DeviceType));
46252
46517
  Object.freeze({
@@ -50210,6 +50475,36 @@ Object.freeze({
50210
50475
  addonId: null,
50211
50476
  access: "view"
50212
50477
  },
50478
+ "oidcConnections.connectionFormSchema": {
50479
+ capName: "oidc-connections",
50480
+ capScope: "system",
50481
+ addonId: null,
50482
+ access: "view"
50483
+ },
50484
+ "oidcConnections.createConnection": {
50485
+ capName: "oidc-connections",
50486
+ capScope: "system",
50487
+ addonId: null,
50488
+ access: "create"
50489
+ },
50490
+ "oidcConnections.deleteConnection": {
50491
+ capName: "oidc-connections",
50492
+ capScope: "system",
50493
+ addonId: null,
50494
+ access: "delete"
50495
+ },
50496
+ "oidcConnections.listConnections": {
50497
+ capName: "oidc-connections",
50498
+ capScope: "system",
50499
+ addonId: null,
50500
+ access: "view"
50501
+ },
50502
+ "oidcConnections.updateConnection": {
50503
+ capName: "oidc-connections",
50504
+ capScope: "system",
50505
+ addonId: null,
50506
+ access: "create"
50507
+ },
50213
50508
  "osd.getStatus": {
50214
50509
  capName: "osd",
50215
50510
  capScope: "device",
@@ -50282,6 +50577,12 @@ Object.freeze({
50282
50577
  addonId: null,
50283
50578
  access: "create"
50284
50579
  },
50580
+ "osdManager.setSlotCameraEnabled": {
50581
+ capName: "osd-manager",
50582
+ capScope: "system",
50583
+ addonId: null,
50584
+ access: "create"
50585
+ },
50285
50586
  "osdManager.upsertTemplate": {
50286
50587
  capName: "osd-manager",
50287
50588
  capScope: "system",
@@ -50420,12 +50721,6 @@ Object.freeze({
50420
50721
  addonId: null,
50421
50722
  access: "create"
50422
50723
  },
50423
- "pipelineAnalytics.createRetrainDatasetVersion": {
50424
- capName: "pipeline-analytics",
50425
- capScope: "device",
50426
- addonId: null,
50427
- access: "create"
50428
- },
50429
50724
  "pipelineAnalytics.deleteDeviceEvents": {
50430
50725
  capName: "pipeline-analytics",
50431
50726
  capScope: "device",
@@ -50438,12 +50733,6 @@ Object.freeze({
50438
50733
  addonId: null,
50439
50734
  access: "delete"
50440
50735
  },
50441
- "pipelineAnalytics.deleteRetrainDatasetVersion": {
50442
- capName: "pipeline-analytics",
50443
- capScope: "device",
50444
- addonId: null,
50445
- access: "delete"
50446
- },
50447
50736
  "pipelineAnalytics.deleteTracks": {
50448
50737
  capName: "pipeline-analytics",
50449
50738
  capScope: "device",
@@ -50546,6 +50835,12 @@ Object.freeze({
50546
50835
  addonId: null,
50547
50836
  access: "view"
50548
50837
  },
50838
+ "pipelineAnalytics.getRetrainCorpusKpi": {
50839
+ capName: "pipeline-analytics",
50840
+ capScope: "device",
50841
+ addonId: null,
50842
+ access: "view"
50843
+ },
50549
50844
  "pipelineAnalytics.getRetrainExportUrl": {
50550
50845
  capName: "pipeline-analytics",
50551
50846
  capScope: "device",
@@ -50666,7 +50961,7 @@ Object.freeze({
50666
50961
  addonId: null,
50667
50962
  access: "view"
50668
50963
  },
50669
- "pipelineAnalytics.listRetrainDatasetVersions": {
50964
+ "pipelineAnalytics.listRetrainCorpusPage": {
50670
50965
  capName: "pipeline-analytics",
50671
50966
  capScope: "device",
50672
50967
  addonId: null,
@@ -50720,12 +51015,6 @@ Object.freeze({
50720
51015
  addonId: null,
50721
51016
  access: "create"
50722
51017
  },
50723
- "pipelineAnalytics.previewRetrainDataset": {
50724
- capName: "pipeline-analytics",
50725
- capScope: "device",
50726
- addonId: null,
50727
- access: "view"
50728
- },
50729
51018
  "pipelineAnalytics.proposeRetrainAnnotations": {
50730
51019
  capName: "pipeline-analytics",
50731
51020
  capScope: "device",
@@ -50804,6 +51093,12 @@ Object.freeze({
50804
51093
  addonId: null,
50805
51094
  access: "create"
50806
51095
  },
51096
+ "pipelineAnalytics.resolveRetrainTrainingCorpus": {
51097
+ capName: "pipeline-analytics",
51098
+ capScope: "device",
51099
+ addonId: null,
51100
+ access: "view"
51101
+ },
50807
51102
  "pipelineAnalytics.restageRetrainTrack": {
50808
51103
  capName: "pipeline-analytics",
50809
51104
  capScope: "device",
@@ -50864,12 +51159,6 @@ Object.freeze({
50864
51159
  addonId: null,
50865
51160
  access: "create"
50866
51161
  },
50867
- "pipelineAnalytics.verifyRetrainDatasetVersion": {
50868
- capName: "pipeline-analytics",
50869
- capScope: "device",
50870
- addonId: null,
50871
- access: "view"
50872
- },
50873
51162
  "pipelineAnalytics.wipeAllAnalytics": {
50874
51163
  capName: "pipeline-analytics",
50875
51164
  capScope: "device",
@@ -53264,18 +53553,6 @@ Object.freeze({
53264
53553
  addonId: null,
53265
53554
  access: "create"
53266
53555
  },
53267
- "training.findDatasetVersionIdByName": {
53268
- capName: "training",
53269
- capScope: "system",
53270
- addonId: null,
53271
- access: "view"
53272
- },
53273
- "training.getDatasetVersion": {
53274
- capName: "training",
53275
- capScope: "system",
53276
- addonId: null,
53277
- access: "view"
53278
- },
53279
53556
  "training.getJob": {
53280
53557
  capName: "training",
53281
53558
  capScope: "system",
@@ -53294,12 +53571,6 @@ Object.freeze({
53294
53571
  addonId: null,
53295
53572
  access: "view"
53296
53573
  },
53297
- "training.listDatasetVersions": {
53298
- capName: "training",
53299
- capScope: "system",
53300
- addonId: null,
53301
- access: "view"
53302
- },
53303
53574
  "training.listJobs": {
53304
53575
  capName: "training",
53305
53576
  capScope: "system",
@@ -53312,54 +53583,66 @@ Object.freeze({
53312
53583
  addonId: null,
53313
53584
  access: "view"
53314
53585
  },
53315
- "training.putDatasetVersion": {
53586
+ "training.registerResult": {
53316
53587
  capName: "training",
53317
53588
  capScope: "system",
53318
53589
  addonId: null,
53319
53590
  access: "create"
53320
53591
  },
53321
- "training.registerResult": {
53592
+ "training.rollbackCanary": {
53322
53593
  capName: "training",
53323
53594
  capScope: "system",
53324
53595
  addonId: null,
53325
53596
  access: "create"
53326
53597
  },
53327
- "training.removeDatasetVersion": {
53598
+ "training.startJob": {
53328
53599
  capName: "training",
53329
53600
  capScope: "system",
53330
53601
  addonId: null,
53331
- access: "delete"
53602
+ access: "create"
53332
53603
  },
53333
- "training.rollbackCanary": {
53604
+ "training.startJobBatch": {
53334
53605
  capName: "training",
53335
53606
  capScope: "system",
53336
53607
  addonId: null,
53337
53608
  access: "create"
53338
53609
  },
53339
- "training.startJob": {
53340
- capName: "training",
53610
+ "trainingProvider.cancelJob": {
53611
+ capName: "training-provider",
53341
53612
  capScope: "system",
53342
53613
  addonId: null,
53343
53614
  access: "create"
53344
53615
  },
53345
- "trainingProvider.cancelJob": {
53616
+ "trainingProvider.cleanupJob": {
53346
53617
  capName: "training-provider",
53347
53618
  capScope: "system",
53348
53619
  addonId: null,
53349
53620
  access: "create"
53350
53621
  },
53351
- "trainingProvider.cleanupJob": {
53622
+ "trainingProvider.createEntry": {
53352
53623
  capName: "training-provider",
53353
53624
  capScope: "system",
53354
53625
  addonId: null,
53355
53626
  access: "create"
53356
53627
  },
53628
+ "trainingProvider.deleteEntry": {
53629
+ capName: "training-provider",
53630
+ capScope: "system",
53631
+ addonId: null,
53632
+ access: "delete"
53633
+ },
53357
53634
  "trainingProvider.describe": {
53358
53635
  capName: "training-provider",
53359
53636
  capScope: "system",
53360
53637
  addonId: null,
53361
53638
  access: "view"
53362
53639
  },
53640
+ "trainingProvider.describeTypes": {
53641
+ capName: "training-provider",
53642
+ capScope: "system",
53643
+ addonId: null,
53644
+ access: "view"
53645
+ },
53363
53646
  "trainingProvider.estimateJob": {
53364
53647
  capName: "training-provider",
53365
53648
  capScope: "system",
@@ -53372,6 +53655,12 @@ Object.freeze({
53372
53655
  addonId: null,
53373
53656
  access: "view"
53374
53657
  },
53658
+ "trainingProvider.listEntries": {
53659
+ capName: "training-provider",
53660
+ capScope: "system",
53661
+ addonId: null,
53662
+ access: "view"
53663
+ },
53375
53664
  "trainingProvider.listJobEvents": {
53376
53665
  capName: "training-provider",
53377
53666
  capScope: "system",
@@ -53402,6 +53691,12 @@ Object.freeze({
53402
53691
  addonId: null,
53403
53692
  access: "create"
53404
53693
  },
53694
+ "trainingProvider.updateEntry": {
53695
+ capName: "training-provider",
53696
+ capScope: "system",
53697
+ addonId: null,
53698
+ access: "create"
53699
+ },
53405
53700
  "trainingProvider.uploadDataset": {
53406
53701
  capName: "training-provider",
53407
53702
  capScope: "system",
@@ -56760,6 +57055,11 @@ Object.freeze({
56760
57055
  form: "single",
56761
57056
  optional: false
56762
57057
  }],
57058
+ "osdManager.setSlotCameraEnabled": [{
57059
+ name: "deviceId",
57060
+ form: "single",
57061
+ optional: false
57062
+ }],
56763
57063
  "petFeeder.callPet": [{
56764
57064
  name: "deviceId",
56765
57065
  form: "single",
@@ -56980,6 +57280,11 @@ Object.freeze({
56980
57280
  form: "array",
56981
57281
  optional: false
56982
57282
  }],
57283
+ "pipelineAnalytics.listRetrainCorpusPage": [{
57284
+ name: "deviceIds",
57285
+ form: "array",
57286
+ optional: true
57287
+ }],
56983
57288
  "pipelineAnalytics.listRetrainStaging": [{
56984
57289
  name: "deviceIds",
56985
57290
  form: "array",
@@ -58089,6 +58394,7 @@ Object.freeze({
58089
58394
  });
58090
58395
  Object.freeze({
58091
58396
  "broker": "broker",
58397
+ "calendar-source": "calendar",
58092
58398
  "device-export": "device-export",
58093
58399
  "llm": "ai",
58094
58400
  "mesh-network": "mesh",
@@ -58213,6 +58519,9 @@ new Map(Object.entries({
58213
58519
  "stream-online": "device-events",
58214
58520
  "stream-offline": "device-events",
58215
58521
  "detection-blind": "device-events",
58522
+ "recording-stalled": "device-events",
58523
+ "recording-healthy": "device-events",
58524
+ "recording-degraded": "device-events",
58216
58525
  "node-online": "system",
58217
58526
  "node-offline": "system",
58218
58527
  "node-restarting": "system",
@@ -58226,6 +58535,9 @@ new Map(Object.entries({
58226
58535
  "wrapper-update-available": "system",
58227
58536
  "backup-completed": "system",
58228
58537
  "export-completed": "system",
58538
+ "storage-location-low": "system",
58539
+ "storage-location-critical": "system",
58540
+ "storage-location-ok": "system",
58229
58541
  "alarm-triggered": "alarm",
58230
58542
  "alarm-armed": "alarm",
58231
58543
  "alarm-disarmed": "alarm",
@@ -58287,6 +58599,16 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
58287
58599
  label: "Detecting nothing",
58288
58600
  kinds: ["detection-blind"]
58289
58601
  },
58602
+ {
58603
+ id: "recording",
58604
+ family: "device-events",
58605
+ label: "Recording",
58606
+ kinds: [
58607
+ "recording-stalled",
58608
+ "recording-healthy",
58609
+ "recording-degraded"
58610
+ ]
58611
+ },
58290
58612
  {
58291
58613
  id: "nodes",
58292
58614
  family: "system",
@@ -58327,6 +58649,16 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
58327
58649
  label: "Backups & exports",
58328
58650
  kinds: ["backup-completed", "export-completed"]
58329
58651
  },
58652
+ {
58653
+ id: "storage",
58654
+ family: "system",
58655
+ label: "Storage locations",
58656
+ kinds: [
58657
+ "storage-location-low",
58658
+ "storage-location-critical",
58659
+ "storage-location-ok"
58660
+ ]
58661
+ },
58330
58662
  {
58331
58663
  id: "alarm",
58332
58664
  family: "alarm",
@@ -58409,6 +58741,9 @@ var DEVICE_EVENTS = [
58409
58741
  "stream-online",
58410
58742
  "stream-offline",
58411
58743
  "detection-blind",
58744
+ "recording-stalled",
58745
+ "recording-healthy",
58746
+ "recording-degraded",
58412
58747
  ...ALARM
58413
58748
  ];
58414
58749
  /** Every kind a summary may speak about — the two summarisable families. */
@@ -58576,6 +58911,11 @@ var STATE_SUMMARY_VARS = [
58576
58911
  [...RULE_TEXT],
58577
58912
  [...RULE_TEXT],
58578
58913
  [...RULE_TEXT],
58914
+ [...RULE_TEXT],
58915
+ [...RULE_TEXT],
58916
+ [...RULE_TEXT],
58917
+ [...RULE_TEXT],
58918
+ [...RULE_TEXT],
58579
58919
  ([...RULE_TEXT], [...UPDATES]),
58580
58920
  ([...RULE_TEXT], [...PACKAGE_UPDATES]),
58581
58921
  ([...RULE_TEXT], [...PACKAGE_UPDATES]),
@@ -71247,7 +71587,7 @@ var require_snapshot_utils = /* @__PURE__ */ require_chunk.__commonJSMin(((expor
71247
71587
  //#region ../../node_modules/undici/lib/mock/snapshot-recorder.js
71248
71588
  var require_snapshot_recorder = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module) => {
71249
71589
  var { writeFile: writeFile$2, readFile: readFile$3, mkdir: mkdir$2 } = require("node:fs/promises");
71250
- var { dirname: dirname$1, resolve } = require("node:path");
71590
+ var { dirname: dirname$2, resolve: resolve$1 } = require("node:path");
71251
71591
  var { setTimeout: setTimeout$1, clearTimeout: clearTimeout$1 } = require("node:timers");
71252
71592
  var { InvalidArgumentError, UndiciError } = require_errors$1();
71253
71593
  var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -71512,7 +71852,7 @@ var require_snapshot_recorder = /* @__PURE__ */ require_chunk.__commonJSMin(((ex
71512
71852
  const path = filePath || this.#snapshotPath;
71513
71853
  if (!path) throw new InvalidArgumentError("Snapshot path is required");
71514
71854
  try {
71515
- const data = await readFile$3(resolve(path), "utf8");
71855
+ const data = await readFile$3(resolve$1(path), "utf8");
71516
71856
  const parsed = JSON.parse(data);
71517
71857
  if (Array.isArray(parsed)) {
71518
71858
  this.#snapshots.clear();
@@ -71532,8 +71872,8 @@ var require_snapshot_recorder = /* @__PURE__ */ require_chunk.__commonJSMin(((ex
71532
71872
  async saveSnapshots(filePath) {
71533
71873
  const path = filePath || this.#snapshotPath;
71534
71874
  if (!path) throw new InvalidArgumentError("Snapshot path is required");
71535
- const resolvedPath = resolve(path);
71536
- await mkdir$2(dirname$1(resolvedPath), { recursive: true });
71875
+ const resolvedPath = resolve$1(path);
71876
+ await mkdir$2(dirname$2(resolvedPath), { recursive: true });
71537
71877
  const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({
71538
71878
  hash,
71539
71879
  snapshot
@@ -160170,6 +160510,7 @@ function buildInitialStatus2(config, enabled) {
160170
160510
  lastErrorMessage: void 0
160171
160511
  };
160172
160512
  }
160513
+ process.platform, process.env["SystemRoot"];
160173
160514
  /**
160174
160515
  * The spawn a one-shot ffmpeg site uses: the spec's seam when one is injected,
160175
160516
  * else the node's PINNED binary (D540), else `null` — and the site refuses by
@@ -160742,6 +161083,13 @@ var reolinkCameraSchema = object({
160742
161083
  */
160743
161084
  clipCatalogForcedWake: boolean().optional(),
160744
161085
  /**
161086
+ * Battery cameras only: the window, in minutes, in which the camera refreshes
161087
+ * ALL its metadata and its cached snapshot exactly once (D847). Absent means
161088
+ * the default (60); `0` turns the scheduled refresh off for this camera; a
161089
+ * value is clamped to 15..240 (below the 6 h presence budget).
161090
+ */
161091
+ batteryRefreshMinutes: number().int().min(0).max(240).optional(),
161092
+ /**
160745
161093
  * Per-AI-class sensitivity overrides — pushed via
160746
161094
  * `setAiDetection(aiType, sensitivity)` (cmdId=343). Reolink
160747
161095
  * range 0..100 — same scale the reolink_aio CGI exposes
@@ -166999,14 +167347,266 @@ function deriveChargingSource(adapterStatus, chargeStatus) {
166999
167347
  function reportsPowerSource(info) {
167000
167348
  return info.adapterStatus !== void 0 || info.chargeStatus !== void 0;
167001
167349
  }
167350
+ /** The settings key, per device. */
167351
+ var BATTERY_REFRESH_MINUTES_KEY = "batteryRefreshMinutes";
167352
+ /** How often the in-memory check runs. Costs nothing: no I/O unless it fires. */
167353
+ var BATTERY_REFRESH_TICK_MS = 6e4;
167354
+ /** The least time between two attempts in one window. */
167355
+ var RETRY_BACKOFF_MS = 10 * 6e4;
167356
+ var MAX_WAKE_LEAD_MS = 5 * 6e4;
167357
+ var STAGGER_SLOTS = 60;
167358
+ /** The operator's window in ms, or `0` for off. Out-of-range values clamp. */
167359
+ function resolveBatteryRefreshIntervalMs(configured) {
167360
+ if (typeof configured !== "number" || !Number.isFinite(configured)) return 60 * 6e4;
167361
+ if (configured <= 0) return 0;
167362
+ return Math.min(240, Math.max(15, Math.round(configured))) * 6e4;
167363
+ }
167364
+ /** Deterministic per-device offset so a fleet's windows do not align. */
167365
+ function staggerOffsetMs(deviceId, intervalMs) {
167366
+ const slot = Math.abs(Math.trunc(deviceId)) % STAGGER_SLOTS;
167367
+ return Math.floor(intervalMs / STAGGER_SLOTS * slot);
167368
+ }
167369
+ /** Which window a wall-clock instant falls in. */
167370
+ function windowIndex(at, intervalMs, offsetMs) {
167371
+ return Math.floor((at - offsetMs) / intervalMs);
167372
+ }
167373
+ /** How long before a window's end the wake is allowed to fire. */
167374
+ function wakeLeadMs(intervalMs) {
167375
+ return Math.min(MAX_WAKE_LEAD_MS, Math.floor(intervalMs / 10));
167376
+ }
167377
+ function decideBatteryRefresh(input) {
167378
+ if (input.intervalMs <= 0) return {
167379
+ kind: "skip",
167380
+ reason: "off"
167381
+ };
167382
+ const current = windowIndex(input.nowMs, input.intervalMs, input.offsetMs);
167383
+ if ((typeof input.lastFullRefreshAt === "number" && input.lastFullRefreshAt > 0 ? windowIndex(input.lastFullRefreshAt, input.intervalMs, input.offsetMs) : Number.NEGATIVE_INFINITY) >= current) return {
167384
+ kind: "skip",
167385
+ reason: "done"
167386
+ };
167387
+ if (input.attemptsInWindow >= 2) return {
167388
+ kind: "skip",
167389
+ reason: "attempts-exhausted"
167390
+ };
167391
+ if (input.lastAttemptAt !== void 0 && input.attemptsInWindow > 0 && input.nowMs - input.lastAttemptAt < 6e5) return {
167392
+ kind: "skip",
167393
+ reason: "backoff"
167394
+ };
167395
+ if (input.awake) return { kind: "refresh-awake" };
167396
+ const windowEnd = input.offsetMs + (current + 1) * input.intervalMs;
167397
+ if (input.nowMs < windowEnd - wakeLeadMs(input.intervalMs)) return {
167398
+ kind: "skip",
167399
+ reason: "asleep-before-deadline"
167400
+ };
167401
+ return { kind: "wake-then-refresh" };
167402
+ }
167403
+ var BatteryHourlyRefresh = class {
167404
+ deps;
167405
+ inFlight = null;
167406
+ attemptWindow = Number.NEGATIVE_INFINITY;
167407
+ attempts = 0;
167408
+ lastAttemptAt;
167409
+ constructor(deps) {
167410
+ this.deps = deps;
167411
+ }
167412
+ /**
167413
+ * The periodic check. Resolves `true` when a refresh landed in this call.
167414
+ * Never throws.
167415
+ */
167416
+ async tick() {
167417
+ return this.consider("tick");
167418
+ }
167419
+ /**
167420
+ * The camera was just observed coming up (event, sleep poll, hub summary, a
167421
+ * confirmed wake). Resolves `true` when this call refreshed everything, so
167422
+ * the caller can skip the parts of its own fan-out the refresh already did.
167423
+ */
167424
+ async noteAwake() {
167425
+ return this.consider("wake-transition");
167426
+ }
167427
+ async consider(trigger) {
167428
+ if (this.inFlight !== null) return this.inFlight;
167429
+ const nowMs = this.deps.now();
167430
+ const intervalMs = this.deps.intervalMs();
167431
+ const offsetMs = intervalMs > 0 ? staggerOffsetMs(this.deps.deviceId, intervalMs) : 0;
167432
+ const window = intervalMs > 0 ? windowIndex(nowMs, intervalMs, offsetMs) : 0;
167433
+ if (window !== this.attemptWindow) {
167434
+ this.attemptWindow = window;
167435
+ this.attempts = 0;
167436
+ this.lastAttemptAt = void 0;
167437
+ }
167438
+ const decision = decideBatteryRefresh({
167439
+ nowMs,
167440
+ intervalMs,
167441
+ offsetMs,
167442
+ lastFullRefreshAt: this.deps.lastFullRefreshAt(),
167443
+ awake: trigger === "wake-transition" || this.deps.isAwake(),
167444
+ attemptsInWindow: this.attempts,
167445
+ lastAttemptAt: this.lastAttemptAt
167446
+ });
167447
+ if (decision.kind === "skip") {
167448
+ this.logSkip(decision.reason, trigger);
167449
+ return false;
167450
+ }
167451
+ this.attempts += 1;
167452
+ this.lastAttemptAt = nowMs;
167453
+ const run = this.run(decision.kind, trigger).finally(() => {
167454
+ this.inFlight = null;
167455
+ });
167456
+ this.inFlight = run;
167457
+ return run;
167458
+ }
167459
+ async run(kind, trigger) {
167460
+ const { logger, deviceId } = this.deps;
167461
+ try {
167462
+ if (kind === "wake-then-refresh") {
167463
+ logger.info("battery hourly refresh: not awake in the window — waking once", {
167464
+ tags: { deviceId },
167465
+ meta: { attempt: this.attempts }
167466
+ });
167467
+ if (!await this.deps.wake()) {
167468
+ logger.warn("battery hourly refresh: the wake did not bring the camera up", {
167469
+ tags: { deviceId },
167470
+ meta: {
167471
+ attempt: this.attempts,
167472
+ retryAfterMs: RETRY_BACKOFF_MS
167473
+ }
167474
+ });
167475
+ return false;
167476
+ }
167477
+ if (this.windowSatisfied()) return true;
167478
+ } else logger.info("battery hourly refresh: camera is awake — refreshing now, no wake", {
167479
+ tags: { deviceId },
167480
+ meta: {
167481
+ trigger,
167482
+ attempt: this.attempts
167483
+ }
167484
+ });
167485
+ const outcome = await this.deps.refreshAll();
167486
+ if (outcome.failed.length > 0) {
167487
+ logger.warn("battery hourly refresh incomplete — not stamped, retried at the next chance", {
167488
+ tags: { deviceId },
167489
+ meta: {
167490
+ failed: outcome.failed,
167491
+ attempt: this.attempts,
167492
+ retryAfterMs: RETRY_BACKOFF_MS
167493
+ }
167494
+ });
167495
+ return false;
167496
+ }
167497
+ const at = this.deps.now();
167498
+ this.deps.stamp(at);
167499
+ logger.info("battery hourly refresh complete", {
167500
+ tags: { deviceId },
167501
+ meta: {
167502
+ kind,
167503
+ trigger,
167504
+ at
167505
+ }
167506
+ });
167507
+ return true;
167508
+ } catch (err) {
167509
+ logger.warn("battery hourly refresh failed — retried at the next chance", {
167510
+ tags: { deviceId },
167511
+ meta: {
167512
+ error: err instanceof Error ? err.message : String(err),
167513
+ attempt: this.attempts,
167514
+ retryAfterMs: RETRY_BACKOFF_MS
167515
+ }
167516
+ });
167517
+ return false;
167518
+ }
167519
+ }
167520
+ windowSatisfied() {
167521
+ const intervalMs = this.deps.intervalMs();
167522
+ if (intervalMs <= 0) return false;
167523
+ const offsetMs = staggerOffsetMs(this.deps.deviceId, intervalMs);
167524
+ const last = this.deps.lastFullRefreshAt();
167525
+ if (typeof last !== "number" || last <= 0) return false;
167526
+ return windowIndex(last, intervalMs, offsetMs) >= windowIndex(this.deps.now(), intervalMs, offsetMs);
167527
+ }
167528
+ logSkip(reason, trigger) {
167529
+ if (reason === "backoff" || reason === "attempts-exhausted") {
167530
+ this.deps.logger.info("battery hourly refresh skipped", {
167531
+ tags: { deviceId: this.deps.deviceId },
167532
+ meta: {
167533
+ reason,
167534
+ trigger,
167535
+ attempts: this.attempts
167536
+ }
167537
+ });
167538
+ return;
167539
+ }
167540
+ this.deps.logger.debug("battery hourly refresh not due", {
167541
+ tags: { deviceId: this.deps.deviceId },
167542
+ meta: {
167543
+ reason,
167544
+ trigger
167545
+ }
167546
+ });
167547
+ }
167548
+ };
167549
+ //#endregion
167550
+ //#region src/battery-refresh-settings.ts
167551
+ var BATTERY_REFRESH_SECTION_ID = "battery-refresh";
167552
+ 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).`;
167553
+ /** Battery cameras only: a mains camera keeps its facts fresh on its own socket. */
167554
+ function buildBatteryRefreshDeviceSection(input) {
167555
+ if (!input.isBattery) return [];
167556
+ return [{
167557
+ id: BATTERY_REFRESH_SECTION_ID,
167558
+ tab: "general",
167559
+ title: "Scheduled refresh",
167560
+ description: "How often this battery camera is guaranteed to have been read, so a battery level never goes stale unseen.",
167561
+ columns: 1,
167562
+ fields: [{
167563
+ type: "number",
167564
+ key: BATTERY_REFRESH_MINUTES_KEY,
167565
+ label: "Refresh window (min)",
167566
+ description: BATTERY_REFRESH_MINUTES_DESCRIPTION,
167567
+ default: 60,
167568
+ min: 0,
167569
+ max: 240,
167570
+ step: 5
167571
+ }]
167572
+ }];
167573
+ }
167002
167574
  //#endregion
167003
- //#region src/battery-contact-expiry.ts
167004
- function shouldWakeForContact(input) {
167005
- if (input.hubChild) return false;
167006
- if (!input.sleeping) return false;
167007
- const last = input.lastContactAt;
167008
- if (typeof last !== "number" || last <= 0) return false;
167009
- return input.nowMs - last + input.tickMs >= input.unreachableAfterMs;
167575
+ //#region src/battery-snapshot-refresh.ts
167576
+ /** The wrapper may run on another node; a stale frame is hours old, skew is seconds. */
167577
+ var CLOCK_SKEW_TOLERANCE_MS = 6e4;
167578
+ async function refreshSnapshotCacheOnce(input) {
167579
+ const { deviceId, api, logger } = input;
167580
+ const startedAt = input.now();
167581
+ if (await api.getSnapshot({
167582
+ deviceId,
167583
+ force: true
167584
+ }) === null) {
167585
+ logger.warn("battery hourly refresh: the snapshot came back empty", { tags: { deviceId } });
167586
+ return false;
167587
+ }
167588
+ let rows;
167589
+ try {
167590
+ rows = await api.getSnapshotOverview({ deviceIds: [deviceId] });
167591
+ } catch (err) {
167592
+ logger.debug("battery hourly refresh: snapshot overview unreadable — counting the image", {
167593
+ tags: { deviceId },
167594
+ meta: { error: err instanceof Error ? err.message : String(err) }
167595
+ });
167596
+ return true;
167597
+ }
167598
+ const capturedAt = rows.find((row) => row.deviceId === deviceId)?.lastCapturedAt ?? null;
167599
+ if (capturedAt === null || capturedAt < startedAt - CLOCK_SKEW_TOLERANCE_MS) {
167600
+ logger.warn("battery hourly refresh: the snapshot cache did not move (a stale frame was served)", {
167601
+ tags: { deviceId },
167602
+ meta: {
167603
+ capturedAt,
167604
+ startedAt
167605
+ }
167606
+ });
167607
+ return false;
167608
+ }
167609
+ return true;
167010
167610
  }
167011
167611
  //#endregion
167012
167612
  //#region src/log-channels.ts
@@ -171009,7 +171609,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
171009
171609
  * passively checks sleep state, and short-circuits if sleeping. */
171010
171610
  batteryUpdateTimer = null;
171011
171611
  /** Single-flight gate so two interval fires can't overlap. */
171012
- batteryUpdateInFlight = null;
171013
171612
  /** Aux accessory references registered by spawned children
171014
171613
  * (siren / floodlight / pir). The parent's centralised align
171015
171614
  * method iterates these to refresh their slices on a single
@@ -171347,7 +171946,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
171347
171946
  * snapshot also drives the dispatch's known-good baseline.
171348
171947
  */
171349
171948
  async refreshParentSettingsSnapshot(slices, reason = "background") {
171350
- if (this.isBattery && this.sleeping && reason !== "post-write") {
171949
+ if (this.isBattery && this.sleeping && reason !== "post-write" && reason !== "scheduled") {
171351
171950
  this.ctx.logger.info("refreshParentSettingsSnapshot skipped — battery cam is sleeping", { tags: {
171352
171951
  deviceId: this.id,
171353
171952
  reason
@@ -172293,7 +172892,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
172293
172892
  tags: { deviceId: this.id },
172294
172893
  meta: { reason }
172295
172894
  });
172296
- return;
172895
+ return false;
172297
172896
  }
172298
172897
  let api;
172299
172898
  try {
@@ -172303,17 +172902,19 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
172303
172902
  tags: { deviceId: this.id },
172304
172903
  meta: { error: err instanceof Error ? err.message : String(err) }
172305
172904
  });
172306
- return;
172905
+ return false;
172307
172906
  }
172308
172907
  try {
172309
172908
  const info = await api.getBatteryInfo(this.getChannel());
172310
172909
  this.updateBatteryCache(info);
172311
172910
  await this.refreshNetworkLinkFromApi(api, "battery");
172911
+ return true;
172312
172912
  } catch (err) {
172313
172913
  this.ctx.logger.debug("battery refresh failed", {
172314
172914
  tags: { deviceId: this.id },
172315
172915
  meta: { error: err instanceof Error ? err.message : String(err) }
172316
172916
  });
172917
+ return false;
172317
172918
  }
172318
172919
  }
172319
172920
  /**
@@ -173559,6 +174160,18 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
173559
174160
  * proactive gate lets it through because `sleeping` is false here.
173560
174161
  * Mirrors the reference's `updateBatteryAndSnapshot`.
173561
174162
  */
174163
+ /** The scheduled refresh's snapshot step: true only when the cache really moved. */
174164
+ refreshSnapshotCache() {
174165
+ return refreshSnapshotCacheOnce({
174166
+ deviceId: this.id,
174167
+ logger: this.ctx.logger,
174168
+ now: () => Date.now(),
174169
+ api: {
174170
+ getSnapshot: (input) => this.ctx.api.snapshot.getSnapshot.query(input),
174171
+ getSnapshotOverview: (input) => this.ctx.api.snapshot.getSnapshotOverview.query({ deviceIds: [...input.deviceIds] })
174172
+ }
174173
+ });
174174
+ }
173562
174175
  async warmSnapshotOnWake() {
173563
174176
  try {
173564
174177
  await this.ctx.api.snapshot.getSnapshot.query({
@@ -175994,119 +176607,84 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
175994
176607
  this.alignAuxTimer = null;
175995
176608
  }
175996
176609
  }
175997
- /** Run one cycle of the battery-cam periodic refresh. Single-flight
175998
- * via `batteryUpdateInFlight`. Never throws. */
175999
- async runBatteryUpdate() {
176000
- if (this.batteryUpdateInFlight) return this.batteryUpdateInFlight;
176001
- const cycle = (async () => {
176610
+ batteryRefresh = null;
176611
+ getBatteryRefresh() {
176612
+ if (this.batteryRefresh === null) this.batteryRefresh = new BatteryHourlyRefresh({
176613
+ deviceId: this.id,
176614
+ logger: this.ctx.logger,
176615
+ now: () => Date.now(),
176616
+ isAwake: () => !this.sleeping,
176617
+ intervalMs: () => resolveBatteryRefreshIntervalMs(this.config.get("batteryRefreshMinutes")),
176618
+ lastFullRefreshAt: () => this.state.battery.lastFullRefreshAt,
176619
+ wake: async () => {
176620
+ if (!this.canProactivelyWake("scheduled-refresh")) return false;
176621
+ this.markWakeIssued();
176622
+ return this.wakeIfSleeping();
176623
+ },
176624
+ refreshAll: () => this.refreshEverythingNow(),
176625
+ stamp: (at) => {
176626
+ this.state.battery.lastFullRefreshAt = at;
176627
+ }
176628
+ });
176629
+ return this.batteryRefresh;
176630
+ }
176631
+ /**
176632
+ * Every fact and the snapshot, read while the camera is up. SERIALIZED (one
176633
+ * Baichuan round-trip at a time on the single BCUDP stream), and each step
176634
+ * names itself in `failed` instead of throwing, so one dead read neither
176635
+ * hides the others nor stamps the window as done.
176636
+ */
176637
+ async refreshEverythingNow() {
176638
+ const failed = [];
176639
+ const step = async (name, run) => {
176002
176640
  try {
176003
- if (this.isBattery && this.sleeping) {
176004
- if (shouldWakeForContact({
176005
- nowMs: Date.now(),
176006
- lastContactAt: this.state.battery.lastContactAt,
176007
- sleeping: true,
176008
- hubChild: this.isHubChild(),
176009
- unreachableAfterMs: 216e5,
176010
- tickMs: ReolinkCamera.BATTERY_UPDATE_INTERVAL_MS
176011
- })) {
176012
- await this.wakeForContactRefresh();
176013
- return;
176014
- }
176015
- this.ctx.logger.debug("battery-update: cam sleeping — skipping cycle without connecting", { tags: { deviceId: this.id } });
176016
- return;
176017
- }
176018
- let api;
176019
- try {
176020
- api = await this.ensureApi();
176021
- } catch (err) {
176022
- this.ctx.logger.debug("battery-update: ensureApi failed — skipping cycle", {
176023
- tags: { deviceId: this.id },
176024
- meta: { error: err instanceof Error ? err.message : String(err) }
176025
- });
176026
- return;
176027
- }
176028
- let sleeping = false;
176029
- try {
176030
- sleeping = api.getSleepStatus({ channel: this.getChannel() }).state === "sleeping";
176031
- } catch (err) {
176032
- this.ctx.logger.debug("battery-update: getSleepStatus threw — skipping cycle", {
176033
- tags: { deviceId: this.id },
176034
- meta: { error: err instanceof Error ? err.message : String(err) }
176035
- });
176036
- return;
176037
- }
176038
- if (sleeping) {
176039
- this.ctx.logger.debug("battery-update: cam sleeping — skipping periodic refresh", { tags: { deviceId: this.id } });
176040
- return;
176041
- }
176042
- this.ctx.logger.info("battery-update: cam awake — running periodic refresh", {
176641
+ if (await run() === false) failed.push(name);
176642
+ } catch (err) {
176643
+ failed.push(name);
176644
+ this.ctx.logger.debug("battery hourly refresh: step threw", {
176043
176645
  tags: { deviceId: this.id },
176044
176646
  meta: {
176045
- isHubChild: this.isHubChild(),
176046
- auxAccessoryCount: this.auxAccessoryRefs.size
176647
+ step: name,
176648
+ error: err instanceof Error ? err.message : String(err)
176047
176649
  }
176048
176650
  });
176049
- await this.refreshBatteryFromApi("periodic").catch(() => {});
176050
- await this.alignAuxDevicesState("periodic").catch(() => {});
176051
- await this.refreshParentSettingsSnapshot().catch(() => {});
176052
- } finally {
176053
- this.batteryUpdateInFlight = null;
176054
176651
  }
176055
- })();
176056
- this.batteryUpdateInFlight = cycle;
176057
- return cycle;
176652
+ };
176653
+ await step("battery", () => this.refreshBatteryFromApi("demand"));
176654
+ await step("device-info", async () => {
176655
+ await this.populateMetadataFromFirmware(await this.ensureApi());
176656
+ });
176657
+ await step("cap-slices", async () => {
176658
+ await this.warmCapSlicesOnWake({ force: true });
176659
+ });
176660
+ await step("aux-accessories", () => this.alignAuxDevicesState("wake"));
176661
+ await step("settings-snapshot", () => this.refreshParentSettingsSnapshot(void 0, "scheduled"));
176662
+ await step("snapshot", () => this.refreshSnapshotCache());
176663
+ return { failed };
176058
176664
  }
176059
176665
  /**
176060
- * D355: wake a standalone battery camera ONCE because its last passive
176061
- * contact is about to expire, and refresh every slice while it is up.
176062
- * Bounded by `canProactivelyWake` (the shared cooldown) and by the fact
176063
- * that a wake that answers resets the silence. A wake that fails is left
176064
- * alone: the silence then reaches the budget and `deriveBatteryPresence`
176065
- * says `unreachable` — for the first time on evidence.
176666
+ * Run one scheduled-refresh check. Never throws; costs nothing unless a
176667
+ * refresh is due (see {@link decideBatteryRefresh}).
176066
176668
  */
176067
- async wakeForContactRefresh() {
176068
- if (!this.canProactivelyWake("contact-expiry")) return;
176069
- this.markWakeIssued();
176070
- const lastContactAt = this.state.battery.lastContactAt ?? 0;
176071
- const silenceMs = Date.now() - lastContactAt;
176072
- this.ctx.logger.info("battery contact expiring — waking once to refresh every slice (D355)", {
176073
- tags: { deviceId: this.id },
176074
- meta: {
176075
- silenceMs,
176076
- budgetMs: BATTERY_UNREACHABLE_AFTER_MS
176077
- }
176078
- });
176079
- try {
176080
- await this.ensureApi();
176081
- } catch (err) {
176082
- this.ctx.logger.warn("battery contact wake FAILED — silence continues and the camera will read unreachable at the budget", {
176083
- tags: { deviceId: this.id },
176084
- meta: {
176085
- silenceMs,
176086
- error: err instanceof Error ? err.message : String(err)
176087
- }
176088
- });
176089
- return;
176090
- }
176091
- await this.refreshBatteryFromApi("demand").catch(() => {});
176092
- await this.warmCapSlicesOnWake().catch(() => {});
176093
- await this.alignAuxDevicesState("wake").catch(() => {});
176094
- await this.warmSnapshotOnWake().catch(() => {});
176669
+ async runBatteryUpdate() {
176670
+ if (!this.isBattery) return;
176671
+ await this.getBatteryRefresh().tick();
176095
176672
  }
176096
- /** Arm the periodic battery-cam refresh timer. No-op for wired cams
176097
- * (their state is kept fresh by the 10 s aux-align poll and push
176098
- * events). Idempotent. */
176673
+ /** Arm the scheduled-refresh check. No-op for wired cams (their state is
176674
+ * kept fresh by the 10 s aux-align poll and push events). Idempotent. */
176099
176675
  startBatteryUpdatePolling() {
176100
176676
  if (this.batteryUpdateTimer) return;
176101
176677
  if (!this.isBattery) return;
176102
176678
  this.batteryUpdateTimer = setInterval(() => {
176103
176679
  this.runBatteryUpdate().catch(() => {});
176104
- }, ReolinkCamera.BATTERY_UPDATE_INTERVAL_MS);
176680
+ }, BATTERY_REFRESH_TICK_MS);
176105
176681
  if (typeof this.batteryUpdateTimer.unref === "function") this.batteryUpdateTimer.unref();
176106
- this.ctx.logger.info("battery-update: periodic refresh armed", {
176682
+ this.ctx.logger.info("battery-update: scheduled full refresh armed", {
176107
176683
  tags: { deviceId: this.id },
176108
176684
  meta: {
176109
- intervalMs: ReolinkCamera.BATTERY_UPDATE_INTERVAL_MS,
176685
+ tickMs: BATTERY_REFRESH_TICK_MS,
176686
+ intervalMs: resolveBatteryRefreshIntervalMs(this.config.get("batteryRefreshMinutes")),
176687
+ lastFullRefreshAt: this.state.battery.lastFullRefreshAt ?? null,
176110
176688
  isHubChild: this.isHubChild()
176111
176689
  }
176112
176690
  });
@@ -176202,22 +176780,18 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
176202
176780
  meta: { error: err instanceof Error ? err.message : String(err) }
176203
176781
  });
176204
176782
  }
176205
- await this.warmCapSlicesOnWake();
176206
- await this.warmSnapshotOnWake();
176207
- await this.alignAuxDevicesState("wake");
176208
- await this.refreshParentSettingsSnapshot();
176783
+ if (!(this.isBattery ? await this.getBatteryRefresh().noteAwake().catch(() => false) : false)) {
176784
+ await this.warmCapSlicesOnWake();
176785
+ await this.warmSnapshotOnWake();
176786
+ await this.alignAuxDevicesState("wake");
176787
+ await this.refreshParentSettingsSnapshot();
176788
+ }
176209
176789
  await this.refreshClipCatalogOnWake();
176210
176790
  }
176211
176791
  static PING_INTERVAL_MS = 3e4;
176212
176792
  static PING_MAX_FAILURES = 3;
176213
176793
  static EVENT_CHECK_INTERVAL_MS = 6e4;
176214
176794
  static EVENT_STALE_THRESHOLD_MS = 10 * 6e4;
176215
- /** Battery-cam periodic refresh cadence. The timer fires on this
176216
- * interval, passively checks sleep state, and only runs the
176217
- * refresh when the cam is already awake — so it never forces a
176218
- * wake. Mirrors the reference's `batteryUpdateIntervalMinutes`
176219
- * default (60 minutes). */
176220
- static BATTERY_UPDATE_INTERVAL_MS = 60 * 6e4;
176221
176795
  /**
176222
176796
  * Arm the connection-liveness ping watchdog and the event-watchdog.
176223
176797
  * Battery cams skip the ping watchdog — they keep `setIdleDisconnect=true`
@@ -177401,7 +177975,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
177401
177975
  ...buildClipRefreshDeviceSection({
177402
177976
  isHubChild: this.isHubChild(),
177403
177977
  isBattery: this.isBattery
177404
- })
177978
+ }),
177979
+ ...buildBatteryRefreshDeviceSection({ isBattery: this.isBattery })
177405
177980
  ]
177406
177981
  };
177407
177982
  const emailSnap = cache?.emailConfigSnapshot;
@@ -177422,6 +177997,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
177422
177997
  emailScheduleEnabled: emailSnap?.taskEnabled ?? false,
177423
177998
  clipCatalogRefreshMinutes: this.config.get("clipCatalogRefreshMinutes"),
177424
177999
  clipCatalogForcedWake: this.config.get("clipCatalogForcedWake") ?? false,
178000
+ batteryRefreshMinutes: this.config.get("batteryRefreshMinutes"),
177425
178001
  debugGeneral: this.config.get("debugGeneral") ?? false,
177426
178002
  debugSocketLogs: this.config.get("debugSocketLogs") ?? [],
177427
178003
  motionEnabled: this.config.get("motionEnabled") ?? motionSnap.enabled ?? true,