@camstack/addon-provider-amcrest 0.1.4 → 0.1.6

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 +304 -233
  2. package/dist/addon.mjs +304 -233
  3. package/package.json +1 -1
package/dist/addon.mjs CHANGED
@@ -4634,7 +4634,7 @@ function _instanceof(cls, params = {}) {
4634
4634
  return inst;
4635
4635
  }
4636
4636
  //#endregion
4637
- //#region ../types/dist/sleep-CZDdRBua.mjs
4637
+ //#region ../types/dist/sleep-Cc14_yxc.mjs
4638
4638
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4639
4639
  EventCategory["SystemBoot"] = "system.boot";
4640
4640
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -4820,6 +4820,18 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
4820
4820
  */
4821
4821
  EventCategory["PipelineCameraUpdated"] = "pipeline.camera-updated";
4822
4822
  /**
4823
+ * The cluster camera-source OWNER changed (`clusterRoles.ingestNode`).
4824
+ * Emitted by addon-pipeline-orchestrator whenever it (re)derives node
4825
+ * capabilities — at boot, on agent online/offline, and on an ingest-node
4826
+ * flip. Carries the resolved `ownerNodeId`. The stream-broker consumes it to
4827
+ * keep its ingest-owner-gate decision current WITHOUT a per-`ensureBroker`
4828
+ * cross-process `getIngestOwner` query (push the authority's decision instead
4829
+ * of polling it on the hot path). Idempotent state — re-emitted on every
4830
+ * topology change, so a dropped event self-heals on the next one (plus the
4831
+ * broker's long backstop reconcile query).
4832
+ */
4833
+ EventCategory["PipelineIngestOwnerChanged"] = "pipeline.ingest-owner-changed";
4834
+ /**
4823
4835
  * Periodic snapshot of per-node pipeline-runner load
4824
4836
  * (`RunnerLocalLoad`). Emitted ~1Hz by every runner so UI dashboards
4825
4837
  * subscribe instead of polling `pipelineRunner.getLocalLoad`.
@@ -6903,6 +6915,36 @@ var ModelFormatsSchema = object({
6903
6915
  tflite: ModelFormatEntrySchema.optional(),
6904
6916
  pt: ModelFormatEntrySchema.optional()
6905
6917
  });
6918
+ /**
6919
+ * Variant-selector grouping axes. Shared by the full `ModelCatalogEntry` and by
6920
+ * the reduced `PipelineModelOption` returned in `pipeline.getSchema()` so the
6921
+ * grouped Family→Tier→Variant picker renders identically in the config UI and
6922
+ * in the pipeline/device steppers. The flat `id` stays the source of truth for
6923
+ * resolution/download/persistence; this is a presentation overlay resolved back
6924
+ * to an `id`.
6925
+ */
6926
+ var ModelVariantGroupSchema = object({
6927
+ /** Top-level family, e.g. `yolo26` (later `d-fine`, `rf-detr`). */
6928
+ family: string(),
6929
+ /** Size within the family, e.g. `n` | `s` | `m` | `l`. */
6930
+ tier: string(),
6931
+ /** Quantization axis. Omit ⇒ the fp32 base build. */
6932
+ precision: _enum(["fp32", "int8"]).optional(),
6933
+ /**
6934
+ * Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
6935
+ * latency-optimized export (e.g. ReLU-activation variant) — the slot the
6936
+ * future performance variants plug into.
6937
+ */
6938
+ optimization: _enum(["standard", "fast"]).optional(),
6939
+ /**
6940
+ * Input-resolution axis (square input side, px). Omit ⇒ the family's native
6941
+ * resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
6942
+ * cheap latency lever — especially on Apple ANE and the Intel N100 — at a
6943
+ * small-object accuracy cost. Mirrors the model's `inputSize` but lifted onto
6944
+ * the group so the selector can offer it as a variant axis.
6945
+ */
6946
+ resolution: number().int().positive().optional()
6947
+ });
6906
6948
  var ModelCatalogEntrySchema = object({
6907
6949
  id: string(),
6908
6950
  name: string(),
@@ -6932,7 +6974,43 @@ var ModelCatalogEntrySchema = object({
6932
6974
  * Auxiliary files required at runtime (labels JSON, charset dict, etc.).
6933
6975
  * Downloaded into the same modelsDir alongside the model file.
6934
6976
  */
6935
- extraFiles: array(ModelExtraFileSchema).readonly().optional()
6977
+ extraFiles: array(ModelExtraFileSchema).readonly().optional(),
6978
+ /**
6979
+ * LEGACY entry — retained in the catalog so a persisted operator selection
6980
+ * still RESOLVES (and can be re-activated), but hidden from the selectable
6981
+ * model list and excluded from the auto format-default pick. Set on the
6982
+ * superseded / consolidated models (older lineages, redundant fp16 IRs) so
6983
+ * the active lineup stays the coherent curated ladder without deleting a
6984
+ * model anyone may still be pinned to. `resolveModelForFormat` keeps honoring
6985
+ * an explicit legacy id that has a build for the node's format.
6986
+ */
6987
+ legacy: boolean().optional(),
6988
+ /**
6989
+ * Measured quality/latency metadata — populated from the benchmark addon on
6990
+ * the real node classes. Absent = not yet measured (most entries today; the
6991
+ * catalog historically carried only `sizeMB`, a poor cross-architecture
6992
+ * speed proxy). `p95LatencyMs` is keyed by node class (e.g. `n100`, `mac`).
6993
+ */
6994
+ metrics: object({
6995
+ map50: number().optional(),
6996
+ p95LatencyMs: record(string(), number()).optional()
6997
+ }).optional(),
6998
+ /**
6999
+ * SPDX-ish license id of the model weights (e.g. `AGPL-3.0` for Ultralytics
7000
+ * YOLO26, `GPL-3.0` for YOLOv9, `Apache-2.0` for D-FINE/RF-DETR). Matters for
7001
+ * the retraining addon and any future commercial distribution.
7002
+ */
7003
+ license: string().optional(),
7004
+ /**
7005
+ * Variant-selector grouping. The UI groups models by `family` + `tier` and
7006
+ * offers `precision` / `optimization` as variant axes WITHIN a tier — so all
7007
+ * of a family's sizes and quantizations collapse into one grouped picker
7008
+ * instead of a flat list of `yolo26s`, `yolo26s-int8`, … Absent ⇒ ungrouped
7009
+ * (legacy / custom models) — never shown in the grouped selector. The flat
7010
+ * `id` stays the source of truth for resolution/download/persistence; grouping
7011
+ * is a presentation overlay resolved back to an `id`.
7012
+ */
7013
+ group: ModelVariantGroupSchema.optional()
6936
7014
  });
6937
7015
  var ConvertTargetSchema = discriminatedUnion("format", [object({
6938
7016
  format: literal("openvino"),
@@ -6993,8 +7071,8 @@ var RecordingModeSchema = _enum([
6993
7071
  "onAudioThreshold"
6994
7072
  ]);
6995
7073
  /**
6996
- * First-class, authoritative per-camera storage mode — the netta choice the UI
6997
- * reads directly (never inferred from `rules`):
7074
+ * First-class, authoritative per-camera storage mode — the explicit choice the
7075
+ * UI reads directly (never inferred from `rules`):
6998
7076
  * - `off` — not recording.
6999
7077
  * - `events` — record only around triggers (motion / audio threshold),
7000
7078
  * with pre/post-buffer.
@@ -8518,6 +8596,72 @@ function createRuntimeStateBridge(params) {
8518
8596
  getStatus
8519
8597
  };
8520
8598
  }
8599
+ /** Reject after `ms`; always clears its own timer. */
8600
+ async function withTimeout(promise, ms, label) {
8601
+ let timer;
8602
+ const timeout = new Promise((_resolve, reject) => {
8603
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${label} timed out after ${String(ms)}ms`)), ms);
8604
+ });
8605
+ try {
8606
+ return await Promise.race([promise, timeout]);
8607
+ } finally {
8608
+ if (timer !== void 0) clearTimeout(timer);
8609
+ }
8610
+ }
8611
+ /**
8612
+ * Start the reachability poll loop. Returns a handle whose `stop()` clears the
8613
+ * timer and prevents any further ticks. Start on device activation, stop on
8614
+ * device teardown (`removeDevice`) so no timer leaks.
8615
+ */
8616
+ function startReachabilityPoll(options) {
8617
+ const intervalMs = options.intervalMs ?? 3e4;
8618
+ const failuresToOffline = options.failuresToOffline ?? 3;
8619
+ const probeTimeoutMs = options.probeTimeoutMs ?? 1e4;
8620
+ const runImmediately = options.runImmediately ?? true;
8621
+ let stopped = false;
8622
+ let running = false;
8623
+ let consecutiveFailures = 0;
8624
+ let timer;
8625
+ const tick = async () => {
8626
+ if (stopped) return;
8627
+ if (running) return;
8628
+ if (options.isEnabled && !options.isEnabled()) return;
8629
+ running = true;
8630
+ try {
8631
+ const reachable = await withTimeout(Promise.resolve().then(options.probe), probeTimeoutMs, "reachability probe");
8632
+ if (stopped) return;
8633
+ if (reachable) {
8634
+ consecutiveFailures = 0;
8635
+ options.setOnline(true);
8636
+ } else registerFailure("probe resolved unreachable");
8637
+ } catch (error) {
8638
+ if (stopped) return;
8639
+ registerFailure(error instanceof Error ? error.message : "probe threw");
8640
+ } finally {
8641
+ running = false;
8642
+ }
8643
+ };
8644
+ const registerFailure = (reason) => {
8645
+ consecutiveFailures += 1;
8646
+ options.logger?.debug("reachability probe failed", {
8647
+ reason,
8648
+ consecutiveFailures,
8649
+ failuresToOffline
8650
+ });
8651
+ if (consecutiveFailures >= failuresToOffline) options.setOnline(false);
8652
+ };
8653
+ timer = setInterval(() => {
8654
+ tick();
8655
+ }, intervalMs);
8656
+ if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
8657
+ if (runImmediately) tick();
8658
+ return { stop: () => {
8659
+ if (stopped) return;
8660
+ stopped = true;
8661
+ if (timer !== void 0) clearInterval(timer);
8662
+ timer = void 0;
8663
+ } };
8664
+ }
8521
8665
  /**
8522
8666
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
8523
8667
  * for every device, regardless of provider — the kernel needs a uniform
@@ -9181,26 +9325,13 @@ onBrightnessChanged: { data: object({
9181
9325
  */
9182
9326
  runtimeState: BrightnessStatusSchema
9183
9327
  };
9328
+ /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
9184
9329
  var StreamFormatSchema = _enum([
9185
9330
  "webrtc",
9186
9331
  "hls",
9187
9332
  "mjpeg",
9188
9333
  "rtsp"
9189
9334
  ]);
9190
- var StreamInfoSchema = object({
9191
- streamId: string(),
9192
- format: StreamFormatSchema,
9193
- url: string().nullable(),
9194
- active: boolean()
9195
- });
9196
- method(object({
9197
- streamId: string(),
9198
- sourceUrl: string(),
9199
- codec: string().optional()
9200
- }), _void(), { kind: "mutation" }), method(object({ streamId: string() }), _void(), { kind: "mutation" }), method(object({
9201
- streamId: string(),
9202
- format: StreamFormatSchema
9203
- }), string().nullable()), method(_void(), array(StreamInfoSchema));
9204
9335
  var RtspRestreamEntrySchema = object({
9205
9336
  brokerId: string(),
9206
9337
  url: string(),
@@ -10068,37 +10199,7 @@ var consumablesCapability = {
10068
10199
  scope: "device",
10069
10200
  deviceNative: true,
10070
10201
  mode: "singleton",
10071
- deviceTypes: [
10072
- DeviceType.Camera,
10073
- DeviceType.Hub,
10074
- DeviceType.Light,
10075
- DeviceType.Siren,
10076
- DeviceType.Switch,
10077
- DeviceType.Sensor,
10078
- DeviceType.Thermostat,
10079
- DeviceType.Button,
10080
- DeviceType.EventEmitter,
10081
- DeviceType.Update,
10082
- DeviceType.Generic,
10083
- DeviceType.Notifier,
10084
- DeviceType.Script,
10085
- DeviceType.Automation,
10086
- DeviceType.Lock,
10087
- DeviceType.Cover,
10088
- DeviceType.Valve,
10089
- DeviceType.Humidifier,
10090
- DeviceType.WaterHeater,
10091
- DeviceType.Fan,
10092
- DeviceType.MediaPlayer,
10093
- DeviceType.AlarmPanel,
10094
- DeviceType.Control,
10095
- DeviceType.Presence,
10096
- DeviceType.Weather,
10097
- DeviceType.Vacuum,
10098
- DeviceType.LawnMower,
10099
- DeviceType.Container,
10100
- DeviceType.Image
10101
- ],
10202
+ deviceTypes: Object.values(DeviceType),
10102
10203
  deviceConfig: { ui: {
10103
10204
  kind: "widget",
10104
10205
  widgetId: "host/consumables-panel",
@@ -11691,7 +11792,6 @@ var PipelineDefaultStepSchema = lazy(() => object({
11691
11792
  enabled: boolean(),
11692
11793
  modelId: string(),
11693
11794
  children: array(PipelineDefaultStepSchema).readonly(),
11694
- engine: PipelineEngineChoiceSchema.optional(),
11695
11795
  group: string().optional(),
11696
11796
  settings: record(string(), unknown()).optional()
11697
11797
  }));
@@ -11716,7 +11816,9 @@ var PipelineModelOptionSchema = object({
11716
11816
  formats: record(string(), object({
11717
11817
  downloaded: boolean(),
11718
11818
  sizeMB: number()
11719
- }))
11819
+ })),
11820
+ group: ModelVariantGroupSchema.optional(),
11821
+ legacy: boolean().optional()
11720
11822
  });
11721
11823
  var ConfigFieldBridge = custom();
11722
11824
  var PipelineAddonSchemaSchema = object({
@@ -11767,15 +11869,42 @@ var EngineProvisioningSchema = object({
11767
11869
  ]),
11768
11870
  progress: number().optional(),
11769
11871
  error: string().optional(),
11770
- nextRetryAt: number().optional()
11872
+ nextRetryAt: number().optional(),
11873
+ /**
11874
+ * Gate A (config-correctness gate at engine change): human-readable
11875
+ * config issues surfaced EAGERLY when the node's engine changes — model
11876
+ * substitutions ("chose X, running Y") and zero-build steps ("no model
11877
+ * has a <format> build"). Additive/optional: informational only, never
11878
+ * enforced here — `assertEngineReady` (readiness) still gates inference.
11879
+ * Absent/empty when the node-default tree resolves cleanly.
11880
+ */
11881
+ configIssues: array(string()).optional()
11771
11882
  });
11772
11883
  var PipelineStepInputSchema = lazy(() => object({
11773
11884
  addonId: string(),
11774
- modelId: string(),
11885
+ modelId: string().optional(),
11775
11886
  enabled: boolean().default(true),
11776
11887
  children: array(PipelineStepInputSchema).optional(),
11777
11888
  settings: record(string(), unknown()).optional()
11778
11889
  }));
11890
+ var ModelSubstitutionSchema = object({
11891
+ addonId: string(),
11892
+ chosen: string(),
11893
+ running: string(),
11894
+ format: string()
11895
+ });
11896
+ var PipelineValidationIssueSchema = object({
11897
+ addonId: string(),
11898
+ kind: _enum(["unknown-addon", "no-format-build"]),
11899
+ detail: string()
11900
+ });
11901
+ var PipelineValidationResultSchema = object({
11902
+ ok: boolean(),
11903
+ issues: array(PipelineValidationIssueSchema).readonly(),
11904
+ substitutions: array(ModelSubstitutionSchema).readonly(),
11905
+ /** The node's `currentEngine.format` this validation ran against. */
11906
+ format: string()
11907
+ });
11779
11908
  var ReferenceImageEntrySchema = object({
11780
11909
  filename: string(),
11781
11910
  stepIds: array(string()).readonly().optional()
@@ -11846,7 +11975,13 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
11846
11975
  })) }), object({ success: literal(true) }), {
11847
11976
  kind: "mutation",
11848
11977
  auth: "admin"
11849
- }), method(_void(), PipelineSchemaSchema), method(_void(), array(PipelineDefaultStepSchema).readonly().nullable()), method(_void(), PipelineConfigBridge), method(_void(), ConfigUISchemaBridge), method(_void(), array(PipelineTemplateSchema$1).readonly()), method(object({
11978
+ }), method(object({ nodeId: string() }), object({
11979
+ success: literal(true),
11980
+ regeneratedModelId: string().nullable()
11981
+ }), {
11982
+ kind: "mutation",
11983
+ auth: "admin"
11984
+ }), method(_void(), PipelineSchemaSchema), method(_void(), array(PipelineDefaultStepSchema).readonly().nullable()), method(_void(), PipelineConfigBridge), method(_void(), ConfigUISchemaBridge), method(object({ steps: array(PipelineStepInputSchema) }), PipelineValidationResultSchema), method(_void(), array(PipelineTemplateSchema$1).readonly()), method(object({
11850
11985
  name: string(),
11851
11986
  steps: array(PipelineTemplateStepSchema).readonly(),
11852
11987
  engine: PipelineEngineChoiceSchema
@@ -12139,6 +12274,13 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
12139
12274
  kind: literal("remote-restream"),
12140
12275
  /** The camera's source-owner node (slice 1: always the hub). */
12141
12276
  ownerNodeId: string(),
12277
+ /**
12278
+ * The owner's LAN-reachable host, resolved by the orchestrator from the
12279
+ * per-node `reachableHost` override (Cluster UI). When present the runner
12280
+ * dials THIS host for the owner's restream, in preference to the
12281
+ * `CAMSTACK_HUB_URL`-derived default. Absent → auto-detect fallback.
12282
+ */
12283
+ ownerReachableHost: string().optional(),
12142
12284
  /** Operator override for the owner host the runner dials. */
12143
12285
  hubHostnameOverride: string().optional()
12144
12286
  })]).describe("Per-camera frame-source mode for the runner (P2c)");
@@ -12147,13 +12289,11 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
12147
12289
  * specific runner instance via `attachCamera`. Carries everything the
12148
12290
  * runner needs to subscribe to the local broker and execute inference.
12149
12291
  *
12150
- * Stateless-pipeline model: the full pipeline content (`engine`, `steps`,
12151
- * optional `audio`) travels with the attach payload. The runner keeps it
12152
- * in RAM for the lifetime of the attach — on rebalance, edit, or
12153
- * restart the orchestrator re-sends the latest snapshot.
12154
- *
12155
- * `engine`/`steps`/`audio` are optional during the additive migration
12156
- * window; once orchestrator + UI are migrated they become required.
12292
+ * Stateless-pipeline model: the pipeline content (`steps`, optional
12293
+ * `audio`) travels with the attach payload. The runner keeps it in RAM
12294
+ * for the lifetime of the attach — on rebalance, edit, or restart the
12295
+ * orchestrator re-sends the latest snapshot. Engine is NOT carried: it is
12296
+ * node-local, resolved by the executing runner at dispatch time.
12157
12297
  */
12158
12298
  var RunnerCameraConfigSchema = object({
12159
12299
  deviceId: number(),
@@ -12204,14 +12344,11 @@ var RunnerCameraConfigSchema = object({
12204
12344
  */
12205
12345
  motionSources: MotionSourcesSchema.default(["analyzer"]),
12206
12346
  pipelineEnabled: boolean().default(true),
12207
- /** Engine choice for video steps (runtime+backend+format). */
12208
- engine: PipelineEngineChoiceSchema.optional(),
12209
12347
  /** Ordered tree of video steps. Absent → runner skips video detection. */
12210
12348
  steps: array(PipelineStepInputSchema).readonly().optional(),
12211
12349
  /** Audio classification branch. `enabled:false` disables, null skips. */
12212
12350
  audio: object({
12213
- engine: PipelineEngineChoiceSchema,
12214
- modelId: string(),
12351
+ modelId: string().optional(),
12215
12352
  enabled: boolean()
12216
12353
  }).nullable().optional(),
12217
12354
  /**
@@ -18074,11 +18211,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18074
18211
  timestamp: number()
18075
18212
  });
18076
18213
  var CameraPipelineConfigSchema = object({
18077
- engine: PipelineEngineChoiceSchema,
18214
+ engine: PipelineEngineChoiceSchema.optional(),
18078
18215
  steps: array(PipelineStepInputSchema).readonly(),
18079
18216
  audio: object({
18080
- engine: PipelineEngineChoiceSchema,
18081
- modelId: string(),
18217
+ engine: PipelineEngineChoiceSchema.optional(),
18218
+ modelId: string().optional(),
18082
18219
  enabled: boolean(),
18083
18220
  settings: record(string(), unknown()).readonly().optional()
18084
18221
  }).nullable().optional()
@@ -18093,7 +18230,7 @@ var PipelineTemplateSchema = object({
18093
18230
  });
18094
18231
  var AgentAddonConfigSchema = object({
18095
18232
  enabled: boolean(),
18096
- modelId: string(),
18233
+ modelId: string().optional(),
18097
18234
  settings: record(string(), unknown()).readonly()
18098
18235
  });
18099
18236
  var AgentPipelineSettingsSchema = object({
@@ -18108,7 +18245,15 @@ var AgentPipelineSettingsSchema = object({
18108
18245
  /** Node is eligible to run audio-analyzer sessions. */
18109
18246
  audio: boolean().optional(),
18110
18247
  /** Node is eligible to be the ingest / source-owner (serve the restream). */
18111
- ingest: boolean().optional()
18248
+ ingest: boolean().optional(),
18249
+ /**
18250
+ * Operator override for the LAN host a cross-node decoder dials to reach
18251
+ * THIS node's restream (Cluster UI). Absent → auto-detect: a remote runner
18252
+ * falls back to its `CAMSTACK_HUB_URL`-derived host (the Moleculer address
18253
+ * it already uses to reach the hub). Set this only when the auto-detected
18254
+ * address is wrong (multi-homed host, NAT, custom interface).
18255
+ */
18256
+ reachableHost: string().optional()
18112
18257
  });
18113
18258
  var CameraPipelineForAgentSchema = object({
18114
18259
  steps: array(PipelineStepInputSchema).readonly(),
@@ -18214,6 +18359,15 @@ var GlobalMetricsSchema = object({
18214
18359
  * capability providers.
18215
18360
  */
18216
18361
  var CapabilityBindingsSchema = record(string(), string());
18362
+ /**
18363
+ * The cluster's single camera-source owner (`clusterRoles.ingestNode`) plus
18364
+ * its LAN-reachable host, if one is registered. See `getIngestOwner`.
18365
+ */
18366
+ var IngestOwnerSchema = object({
18367
+ ownerNodeId: string(),
18368
+ reachableHost: string().optional(),
18369
+ configIssue: string().optional()
18370
+ });
18217
18371
  /** Source block — always present; derives from the stream catalog. */
18218
18372
  var CameraSourceStatusSchema = object({ streams: array(object({
18219
18373
  camStreamId: string(),
@@ -18228,6 +18382,14 @@ var CameraAssignmentStatusSchema = object({
18228
18382
  detectionNodeId: string().nullable(),
18229
18383
  decoderNodeId: string().nullable(),
18230
18384
  audioNodeId: string().nullable(),
18385
+ /**
18386
+ * The node that OWNS this camera's physical source pull (dials the RTSP and
18387
+ * hosts the broker/restream) — the cluster ingest owner today
18388
+ * (`clusterRoles.ingestNode`), per-camera once source assignment lands. Lets
18389
+ * the UI show WHERE a camera is sourced without SSH/logs, and is the node the
18390
+ * broker block below was read from (pinned). Nullable only pre-wiring.
18391
+ */
18392
+ sourceNodeId: string().nullable(),
18231
18393
  pinned: object({
18232
18394
  detection: boolean(),
18233
18395
  decoder: boolean(),
@@ -18360,7 +18522,7 @@ method(object({
18360
18522
  }), object({ success: literal(true) }), {
18361
18523
  kind: "mutation",
18362
18524
  auth: "admin"
18363
- }), method(object({
18525
+ }), method(_void(), IngestOwnerSchema), method(object({
18364
18526
  deviceId: number(),
18365
18527
  nodeId: string()
18366
18528
  }), _void(), {
@@ -18429,6 +18591,12 @@ method(object({
18429
18591
  }), object({ success: literal(true) }), {
18430
18592
  kind: "mutation",
18431
18593
  auth: "admin"
18594
+ }), method(object({
18595
+ agentNodeId: string(),
18596
+ reachableHost: string().nullable()
18597
+ }), object({ success: literal(true) }), {
18598
+ kind: "mutation",
18599
+ auth: "admin"
18432
18600
  }), method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()), method(object({
18433
18601
  deviceId: number(),
18434
18602
  addonId: string(),
@@ -18473,22 +18641,6 @@ method(object({
18473
18641
  kind: "mutation",
18474
18642
  auth: "admin"
18475
18643
  });
18476
- var RegisteredStreamSchema = object({
18477
- streamId: string(),
18478
- label: string().optional(),
18479
- codec: string(),
18480
- type: _enum(["video", "audio"]),
18481
- sourceUrl: string()
18482
- });
18483
- var ExposedResourceSchema = object({
18484
- streamId: string(),
18485
- format: string(),
18486
- value: string()
18487
- });
18488
- method(object({
18489
- deviceId: number(),
18490
- streams: array(RegisteredStreamSchema).readonly()
18491
- }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), array(ExposedResourceSchema).readonly());
18492
18644
  /**
18493
18645
  * Query filter for settings-store collections.
18494
18646
  */
@@ -18641,9 +18793,9 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
18641
18793
  /**
18642
18794
  * A single device snapshot returned as base64 JPEG/PNG.
18643
18795
  *
18644
- * Shared with the `snapshot-provider` collection cap the orchestrator
18645
- * receives the same shape from each native provider and from the
18646
- * broker-based fallback.
18796
+ * The `SnapshotAddon` wrapper returns this shape whether the frame came from
18797
+ * the device-native provider (onboard capture) or from the stream-broker
18798
+ * prebuffer fallback.
18647
18799
  */
18648
18800
  var SnapshotImageSchema = object({
18649
18801
  base64: string(),
@@ -18718,10 +18870,6 @@ var snapshotCapability = {
18718
18870
  kind: "poll"
18719
18871
  }
18720
18872
  };
18721
- method(object({ deviceId: number() }), boolean()), method(object({
18722
- deviceId: number(),
18723
- streamId: string().optional()
18724
- }), SnapshotImageSchema.nullable());
18725
18873
  /**
18726
18874
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
18727
18875
  * providers (OIDC, SAML, magic-link, …) mint an HMAC-signed token
@@ -19082,9 +19230,10 @@ method(object({
19082
19230
  auth: "admin"
19083
19231
  });
19084
19232
  /**
19085
- * Optional client-side hints sent at session creation to help the
19086
- * provider pick the best native source. All fields are optional —
19087
- * a viewer that knows nothing still gets a sane default.
19233
+ * Optional client-side hints sent at session creation to help the provider
19234
+ * pick the best native source. All fields optional — a viewer that knows
19235
+ * nothing still gets a sane default. (Relocated from the retired `webrtc`
19236
+ * collection cap; this `webrtc-session` cap is the live signaling surface.)
19088
19237
  */
19089
19238
  var webrtcClientHintsSchema = object({
19090
19239
  viewportWidth: number().int().positive().optional(),
@@ -19095,22 +19244,6 @@ var webrtcClientHintsSchema = object({
19095
19244
  /** Hard tier override; takes precedence over scoring when registered. */
19096
19245
  prefersTier: string().optional()
19097
19246
  }).partial();
19098
- method(object({
19099
- streamId: string(),
19100
- sdpOffer: string()
19101
- }), string(), { kind: "mutation" }), method(object({ streamId: string() }), boolean()), method(object({
19102
- streamId: string(),
19103
- codec: string()
19104
- }), _void(), { kind: "mutation" }), method(object({ streamId: string() }), _void(), { kind: "mutation" }), method(object({
19105
- streamId: string(),
19106
- hints: webrtcClientHintsSchema.optional()
19107
- }), object({
19108
- sessionId: string(),
19109
- sdpOffer: string()
19110
- }), { kind: "mutation" }), method(object({
19111
- sessionId: string(),
19112
- sdpAnswer: string()
19113
- }), _void(), { kind: "mutation" }), method(object({ sessionId: string() }), _void(), { kind: "mutation" }), method(object({ streamId: string() }), boolean());
19114
19247
  /**
19115
19248
  * Discriminated target for a WebRTC session. The client sends this
19116
19249
  * structured object instead of building / parsing brokerId strings;
@@ -24452,6 +24585,12 @@ Object.freeze({
24452
24585
  addonId: null,
24453
24586
  access: "create"
24454
24587
  },
24588
+ "pipelineExecutor.resetToDefault": {
24589
+ capName: "pipeline-executor",
24590
+ capScope: "system",
24591
+ addonId: null,
24592
+ access: "delete"
24593
+ },
24455
24594
  "pipelineExecutor.runAudioTest": {
24456
24595
  capName: "pipeline-executor",
24457
24596
  capScope: "system",
@@ -24500,6 +24639,12 @@ Object.freeze({
24500
24639
  addonId: null,
24501
24640
  access: "create"
24502
24641
  },
24642
+ "pipelineExecutor.validatePipeline": {
24643
+ capName: "pipeline-executor",
24644
+ capScope: "system",
24645
+ addonId: null,
24646
+ access: "view"
24647
+ },
24503
24648
  "pipelineOrchestrator.assignAudio": {
24504
24649
  capName: "pipeline-orchestrator",
24505
24650
  capScope: "system",
@@ -24608,6 +24753,12 @@ Object.freeze({
24608
24753
  addonId: null,
24609
24754
  access: "view"
24610
24755
  },
24756
+ "pipelineOrchestrator.getIngestOwner": {
24757
+ capName: "pipeline-orchestrator",
24758
+ capScope: "system",
24759
+ addonId: null,
24760
+ access: "view"
24761
+ },
24611
24762
  "pipelineOrchestrator.getPipelineAssignment": {
24612
24763
  capName: "pipeline-orchestrator",
24613
24764
  capScope: "system",
@@ -24680,6 +24831,12 @@ Object.freeze({
24680
24831
  addonId: null,
24681
24832
  access: "create"
24682
24833
  },
24834
+ "pipelineOrchestrator.setAgentReachableHost": {
24835
+ capName: "pipeline-orchestrator",
24836
+ capScope: "system",
24837
+ addonId: null,
24838
+ access: "create"
24839
+ },
24683
24840
  "pipelineOrchestrator.setCameraPipelineForAgent": {
24684
24841
  capName: "pipeline-orchestrator",
24685
24842
  capScope: "system",
@@ -25010,24 +25167,6 @@ Object.freeze({
25010
25167
  addonId: null,
25011
25168
  access: "create"
25012
25169
  },
25013
- "restreamer.getExposedResources": {
25014
- capName: "restreamer",
25015
- capScope: "system",
25016
- addonId: null,
25017
- access: "view"
25018
- },
25019
- "restreamer.registerDevice": {
25020
- capName: "restreamer",
25021
- capScope: "system",
25022
- addonId: null,
25023
- access: "create"
25024
- },
25025
- "restreamer.unregisterDevice": {
25026
- capName: "restreamer",
25027
- capScope: "system",
25028
- addonId: null,
25029
- access: "delete"
25030
- },
25031
25170
  "scriptRunner.run": {
25032
25171
  capName: "script-runner",
25033
25172
  capScope: "device",
@@ -25130,18 +25269,6 @@ Object.freeze({
25130
25269
  addonId: null,
25131
25270
  access: "create"
25132
25271
  },
25133
- "snapshotProvider.getSnapshot": {
25134
- capName: "snapshot-provider",
25135
- capScope: "system",
25136
- addonId: null,
25137
- access: "view"
25138
- },
25139
- "snapshotProvider.supportsDevice": {
25140
- capName: "snapshot-provider",
25141
- capScope: "system",
25142
- addonId: null,
25143
- access: "view"
25144
- },
25145
25272
  "ssoBridge.signBridgeToken": {
25146
25273
  capName: "sso-bridge",
25147
25274
  capScope: "system",
@@ -25568,30 +25695,6 @@ Object.freeze({
25568
25695
  addonId: null,
25569
25696
  access: "view"
25570
25697
  },
25571
- "streamingEngine.getStreamUrl": {
25572
- capName: "streaming-engine",
25573
- capScope: "system",
25574
- addonId: null,
25575
- access: "view"
25576
- },
25577
- "streamingEngine.listStreams": {
25578
- capName: "streaming-engine",
25579
- capScope: "system",
25580
- addonId: null,
25581
- access: "view"
25582
- },
25583
- "streamingEngine.registerStream": {
25584
- capName: "streaming-engine",
25585
- capScope: "system",
25586
- addonId: null,
25587
- access: "create"
25588
- },
25589
- "streamingEngine.unregisterStream": {
25590
- capName: "streaming-engine",
25591
- capScope: "system",
25592
- addonId: null,
25593
- access: "delete"
25594
- },
25595
25698
  "streamParams.getConfigSchema": {
25596
25699
  capName: "stream-params",
25597
25700
  capScope: "device",
@@ -25958,54 +26061,6 @@ Object.freeze({
25958
26061
  addonId: null,
25959
26062
  access: "create"
25960
26063
  },
25961
- "webrtc.closeSession": {
25962
- capName: "webrtc",
25963
- capScope: "system",
25964
- addonId: null,
25965
- access: "create"
25966
- },
25967
- "webrtc.createSession": {
25968
- capName: "webrtc",
25969
- capScope: "system",
25970
- addonId: null,
25971
- access: "create"
25972
- },
25973
- "webrtc.handleAnswer": {
25974
- capName: "webrtc",
25975
- capScope: "system",
25976
- addonId: null,
25977
- access: "create"
25978
- },
25979
- "webrtc.handleOffer": {
25980
- capName: "webrtc",
25981
- capScope: "system",
25982
- addonId: null,
25983
- access: "create"
25984
- },
25985
- "webrtc.hasAdaptiveBitrate": {
25986
- capName: "webrtc",
25987
- capScope: "system",
25988
- addonId: null,
25989
- access: "view"
25990
- },
25991
- "webrtc.registerStream": {
25992
- capName: "webrtc",
25993
- capScope: "system",
25994
- addonId: null,
25995
- access: "create"
25996
- },
25997
- "webrtc.supportsStream": {
25998
- capName: "webrtc",
25999
- capScope: "system",
26000
- addonId: null,
26001
- access: "view"
26002
- },
26003
- "webrtc.unregisterStream": {
26004
- capName: "webrtc",
26005
- capScope: "system",
26006
- addonId: null,
26007
- access: "delete"
26008
- },
26009
26064
  "webrtcSession.addIceCandidate": {
26010
26065
  capName: "webrtc-session",
26011
26066
  capScope: "device",
@@ -26757,6 +26812,10 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
26757
26812
  motionActive = false;
26758
26813
  /** Keepalive re-emit timer for a sustained motion window. */
26759
26814
  motionKeepaliveTimer = null;
26815
+ /** Control-plane reachability poll — drives `device.online` from Dahua CGI
26816
+ * `getDeviceInfo` liveness, decoupled from stream-broker video health.
26817
+ * Started in `onActivate`, stopped in `removeDevice`. */
26818
+ reachabilityPoll = null;
26760
26819
  /** Single-flight guard for the `stream-params` camera refresh. */
26761
26820
  streamParamsRefreshInFlight = null;
26762
26821
  /** Single-flight guard for the `motion-zones` camera refresh. */
@@ -26929,12 +26988,33 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
26929
26988
  /** Phase 5 — device is live: open the onboard-motion event stream. */
26930
26989
  async onActivate() {
26931
26990
  this.ensureEventSubscription();
26991
+ this.startReachabilityPolling();
26992
+ }
26993
+ /** Start the control-plane reachability poll: a Dahua CGI `getDeviceInfo`
26994
+ * round-trip every 30s drives `device.online`, with hysteresis. Replaces
26995
+ * the old stream-health→online mirror so an on-demand (idle) but reachable
26996
+ * camera still reports ONLINE. Idempotent. */
26997
+ startReachabilityPolling() {
26998
+ if (this.reachabilityPoll) return;
26999
+ this.reachabilityPoll = startReachabilityPoll({
27000
+ probe: async () => {
27001
+ await this.ensureClient().getDeviceInfo();
27002
+ return true;
27003
+ },
27004
+ setOnline: (online) => {
27005
+ this.markOnline(online);
27006
+ },
27007
+ isEnabled: () => !this.disabled,
27008
+ logger: this.ctx.logger
27009
+ });
26932
27010
  }
26933
27011
  /** Teardown — stop the stream + motion timers, drop the client. */
26934
27012
  async removeDevice() {
26935
27013
  this.ctx.logger.info("Removing Amcrest camera", { tags: { deviceId: this.id } });
26936
27014
  this.teardownEventSubscription();
26937
27015
  this.clearPtzAutoStop();
27016
+ this.reachabilityPoll?.stop();
27017
+ this.reachabilityPoll = null;
26938
27018
  this.client = null;
26939
27019
  }
26940
27020
  registerStreamCatalogProvider() {
@@ -27185,9 +27265,14 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
27185
27265
  if (deviceId !== this.id) return;
27186
27266
  const client = this.ensureClient();
27187
27267
  const requested = Number.parseInt(presetId, 10);
27188
- const used = new Set((await client.getPtzPresets(this.channel)).map((p) => p.id));
27189
- let slot = Number.isFinite(requested) && requested >= 1 && !used.has(requested) ? requested : 1;
27190
- while (used.has(slot)) slot += 1;
27268
+ const DAHUA_MAX_PRESETS = 25;
27269
+ let slot;
27270
+ if (Number.isInteger(requested) && requested >= 1 && requested <= DAHUA_MAX_PRESETS) slot = requested;
27271
+ else {
27272
+ const used = new Set((await client.getPtzPresets(this.channel)).map((p) => p.id));
27273
+ slot = 1;
27274
+ while (used.has(slot)) slot += 1;
27275
+ }
27191
27276
  await client.ptzSetPreset(this.channel, slot);
27192
27277
  },
27193
27278
  deletePreset: async ({ deviceId, presetId }) => {
@@ -36950,21 +37035,7 @@ var AmcrestProviderAddon = class extends BaseDeviceProvider {
36950
37035
  throw new Error(`Amcrest: probe on ${host || "(unknown host)"} resolved neither mac nor host address — cannot persist a stable row key. Verify network reachability + credentials, then retry.`);
36951
37036
  }
36952
37037
  async onInitialize() {
36953
- const regs = await super.onInitialize();
36954
- this.subscribe({ category: EventCategory.DeviceStateChanged }, (event) => {
36955
- const data = event.data;
36956
- if (data.capName !== "camera-streams") return;
36957
- const deviceId = data.deviceId;
36958
- if (typeof deviceId !== "number") return;
36959
- const registry = this.ctx.kernel.deviceRegistry;
36960
- if (!registry) return;
36961
- if (registry.getAddonId(deviceId) !== this.addonId) return;
36962
- const device = registry.getById(deviceId);
36963
- if (!device) return;
36964
- const online = data.slice?.online === true;
36965
- if (device.online !== online) device.online = online;
36966
- });
36967
- return regs;
37038
+ return await super.onInitialize();
36968
37039
  }
36969
37040
  async supportsDiscovery() {
36970
37041
  return true;