@camstack/addon-provider-amcrest 0.1.3 → 0.1.5

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 +207 -226
  2. package/dist/addon.mjs +207 -226
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -4633,7 +4633,7 @@ function _instanceof(cls, params = {}) {
4633
4633
  return inst;
4634
4634
  }
4635
4635
  //#endregion
4636
- //#region ../types/dist/sleep-CZDdRBua.mjs
4636
+ //#region ../types/dist/sleep-Cc14_yxc.mjs
4637
4637
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4638
4638
  EventCategory["SystemBoot"] = "system.boot";
4639
4639
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -4819,6 +4819,18 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
4819
4819
  */
4820
4820
  EventCategory["PipelineCameraUpdated"] = "pipeline.camera-updated";
4821
4821
  /**
4822
+ * The cluster camera-source OWNER changed (`clusterRoles.ingestNode`).
4823
+ * Emitted by addon-pipeline-orchestrator whenever it (re)derives node
4824
+ * capabilities — at boot, on agent online/offline, and on an ingest-node
4825
+ * flip. Carries the resolved `ownerNodeId`. The stream-broker consumes it to
4826
+ * keep its ingest-owner-gate decision current WITHOUT a per-`ensureBroker`
4827
+ * cross-process `getIngestOwner` query (push the authority's decision instead
4828
+ * of polling it on the hot path). Idempotent state — re-emitted on every
4829
+ * topology change, so a dropped event self-heals on the next one (plus the
4830
+ * broker's long backstop reconcile query).
4831
+ */
4832
+ EventCategory["PipelineIngestOwnerChanged"] = "pipeline.ingest-owner-changed";
4833
+ /**
4822
4834
  * Periodic snapshot of per-node pipeline-runner load
4823
4835
  * (`RunnerLocalLoad`). Emitted ~1Hz by every runner so UI dashboards
4824
4836
  * subscribe instead of polling `pipelineRunner.getLocalLoad`.
@@ -6902,6 +6914,28 @@ var ModelFormatsSchema = object({
6902
6914
  tflite: ModelFormatEntrySchema.optional(),
6903
6915
  pt: ModelFormatEntrySchema.optional()
6904
6916
  });
6917
+ /**
6918
+ * Variant-selector grouping axes. Shared by the full `ModelCatalogEntry` and by
6919
+ * the reduced `PipelineModelOption` returned in `pipeline.getSchema()` so the
6920
+ * grouped Family→Tier→Variant picker renders identically in the config UI and
6921
+ * in the pipeline/device steppers. The flat `id` stays the source of truth for
6922
+ * resolution/download/persistence; this is a presentation overlay resolved back
6923
+ * to an `id`.
6924
+ */
6925
+ var ModelVariantGroupSchema = object({
6926
+ /** Top-level family, e.g. `yolo26` (later `d-fine`, `rf-detr`). */
6927
+ family: string(),
6928
+ /** Size within the family, e.g. `n` | `s` | `m` | `l`. */
6929
+ tier: string(),
6930
+ /** Quantization axis. Omit ⇒ the fp32 base build. */
6931
+ precision: _enum(["fp32", "int8"]).optional(),
6932
+ /**
6933
+ * Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
6934
+ * latency-optimized export (e.g. ReLU-activation / reduced-input variant)
6935
+ * — the slot the future performance variants plug into.
6936
+ */
6937
+ optimization: _enum(["standard", "fast"]).optional()
6938
+ });
6905
6939
  var ModelCatalogEntrySchema = object({
6906
6940
  id: string(),
6907
6941
  name: string(),
@@ -6931,7 +6965,43 @@ var ModelCatalogEntrySchema = object({
6931
6965
  * Auxiliary files required at runtime (labels JSON, charset dict, etc.).
6932
6966
  * Downloaded into the same modelsDir alongside the model file.
6933
6967
  */
6934
- extraFiles: array(ModelExtraFileSchema).readonly().optional()
6968
+ extraFiles: array(ModelExtraFileSchema).readonly().optional(),
6969
+ /**
6970
+ * LEGACY entry — retained in the catalog so a persisted operator selection
6971
+ * still RESOLVES (and can be re-activated), but hidden from the selectable
6972
+ * model list and excluded from the auto format-default pick. Set on the
6973
+ * superseded / consolidated models (older lineages, redundant fp16 IRs) so
6974
+ * the active lineup stays the coherent curated ladder without deleting a
6975
+ * model anyone may still be pinned to. `resolveModelForFormat` keeps honoring
6976
+ * an explicit legacy id that has a build for the node's format.
6977
+ */
6978
+ legacy: boolean().optional(),
6979
+ /**
6980
+ * Measured quality/latency metadata — populated from the benchmark addon on
6981
+ * the real node classes. Absent = not yet measured (most entries today; the
6982
+ * catalog historically carried only `sizeMB`, a poor cross-architecture
6983
+ * speed proxy). `p95LatencyMs` is keyed by node class (e.g. `n100`, `mac`).
6984
+ */
6985
+ metrics: object({
6986
+ map50: number().optional(),
6987
+ p95LatencyMs: record(string(), number()).optional()
6988
+ }).optional(),
6989
+ /**
6990
+ * SPDX-ish license id of the model weights (e.g. `AGPL-3.0` for Ultralytics
6991
+ * YOLO26, `GPL-3.0` for YOLOv9, `Apache-2.0` for D-FINE/RF-DETR). Matters for
6992
+ * the retraining addon and any future commercial distribution.
6993
+ */
6994
+ license: string().optional(),
6995
+ /**
6996
+ * Variant-selector grouping. The UI groups models by `family` + `tier` and
6997
+ * offers `precision` / `optimization` as variant axes WITHIN a tier — so all
6998
+ * of a family's sizes and quantizations collapse into one grouped picker
6999
+ * instead of a flat list of `yolo26s`, `yolo26s-int8`, … Absent ⇒ ungrouped
7000
+ * (legacy / custom models) — never shown in the grouped selector. The flat
7001
+ * `id` stays the source of truth for resolution/download/persistence; grouping
7002
+ * is a presentation overlay resolved back to an `id`.
7003
+ */
7004
+ group: ModelVariantGroupSchema.optional()
6935
7005
  });
6936
7006
  var ConvertTargetSchema = discriminatedUnion("format", [object({
6937
7007
  format: literal("openvino"),
@@ -6992,8 +7062,8 @@ var RecordingModeSchema = _enum([
6992
7062
  "onAudioThreshold"
6993
7063
  ]);
6994
7064
  /**
6995
- * First-class, authoritative per-camera storage mode — the netta choice the UI
6996
- * reads directly (never inferred from `rules`):
7065
+ * First-class, authoritative per-camera storage mode — the explicit choice the
7066
+ * UI reads directly (never inferred from `rules`):
6997
7067
  * - `off` — not recording.
6998
7068
  * - `events` — record only around triggers (motion / audio threshold),
6999
7069
  * with pre/post-buffer.
@@ -9180,26 +9250,13 @@ onBrightnessChanged: { data: object({
9180
9250
  */
9181
9251
  runtimeState: BrightnessStatusSchema
9182
9252
  };
9253
+ /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
9183
9254
  var StreamFormatSchema = _enum([
9184
9255
  "webrtc",
9185
9256
  "hls",
9186
9257
  "mjpeg",
9187
9258
  "rtsp"
9188
9259
  ]);
9189
- var StreamInfoSchema = object({
9190
- streamId: string(),
9191
- format: StreamFormatSchema,
9192
- url: string().nullable(),
9193
- active: boolean()
9194
- });
9195
- method(object({
9196
- streamId: string(),
9197
- sourceUrl: string(),
9198
- codec: string().optional()
9199
- }), _void(), { kind: "mutation" }), method(object({ streamId: string() }), _void(), { kind: "mutation" }), method(object({
9200
- streamId: string(),
9201
- format: StreamFormatSchema
9202
- }), string().nullable()), method(_void(), array(StreamInfoSchema));
9203
9260
  var RtspRestreamEntrySchema = object({
9204
9261
  brokerId: string(),
9205
9262
  url: string(),
@@ -10067,37 +10124,7 @@ var consumablesCapability = {
10067
10124
  scope: "device",
10068
10125
  deviceNative: true,
10069
10126
  mode: "singleton",
10070
- deviceTypes: [
10071
- DeviceType.Camera,
10072
- DeviceType.Hub,
10073
- DeviceType.Light,
10074
- DeviceType.Siren,
10075
- DeviceType.Switch,
10076
- DeviceType.Sensor,
10077
- DeviceType.Thermostat,
10078
- DeviceType.Button,
10079
- DeviceType.EventEmitter,
10080
- DeviceType.Update,
10081
- DeviceType.Generic,
10082
- DeviceType.Notifier,
10083
- DeviceType.Script,
10084
- DeviceType.Automation,
10085
- DeviceType.Lock,
10086
- DeviceType.Cover,
10087
- DeviceType.Valve,
10088
- DeviceType.Humidifier,
10089
- DeviceType.WaterHeater,
10090
- DeviceType.Fan,
10091
- DeviceType.MediaPlayer,
10092
- DeviceType.AlarmPanel,
10093
- DeviceType.Control,
10094
- DeviceType.Presence,
10095
- DeviceType.Weather,
10096
- DeviceType.Vacuum,
10097
- DeviceType.LawnMower,
10098
- DeviceType.Container,
10099
- DeviceType.Image
10100
- ],
10127
+ deviceTypes: Object.values(DeviceType),
10101
10128
  deviceConfig: { ui: {
10102
10129
  kind: "widget",
10103
10130
  widgetId: "host/consumables-panel",
@@ -11690,7 +11717,6 @@ var PipelineDefaultStepSchema = lazy(() => object({
11690
11717
  enabled: boolean(),
11691
11718
  modelId: string(),
11692
11719
  children: array(PipelineDefaultStepSchema).readonly(),
11693
- engine: PipelineEngineChoiceSchema.optional(),
11694
11720
  group: string().optional(),
11695
11721
  settings: record(string(), unknown()).optional()
11696
11722
  }));
@@ -11715,7 +11741,9 @@ var PipelineModelOptionSchema = object({
11715
11741
  formats: record(string(), object({
11716
11742
  downloaded: boolean(),
11717
11743
  sizeMB: number()
11718
- }))
11744
+ })),
11745
+ group: ModelVariantGroupSchema.optional(),
11746
+ legacy: boolean().optional()
11719
11747
  });
11720
11748
  var ConfigFieldBridge = custom();
11721
11749
  var PipelineAddonSchemaSchema = object({
@@ -11766,15 +11794,42 @@ var EngineProvisioningSchema = object({
11766
11794
  ]),
11767
11795
  progress: number().optional(),
11768
11796
  error: string().optional(),
11769
- nextRetryAt: number().optional()
11797
+ nextRetryAt: number().optional(),
11798
+ /**
11799
+ * Gate A (config-correctness gate at engine change): human-readable
11800
+ * config issues surfaced EAGERLY when the node's engine changes — model
11801
+ * substitutions ("chose X, running Y") and zero-build steps ("no model
11802
+ * has a <format> build"). Additive/optional: informational only, never
11803
+ * enforced here — `assertEngineReady` (readiness) still gates inference.
11804
+ * Absent/empty when the node-default tree resolves cleanly.
11805
+ */
11806
+ configIssues: array(string()).optional()
11770
11807
  });
11771
11808
  var PipelineStepInputSchema = lazy(() => object({
11772
11809
  addonId: string(),
11773
- modelId: string(),
11810
+ modelId: string().optional(),
11774
11811
  enabled: boolean().default(true),
11775
11812
  children: array(PipelineStepInputSchema).optional(),
11776
11813
  settings: record(string(), unknown()).optional()
11777
11814
  }));
11815
+ var ModelSubstitutionSchema = object({
11816
+ addonId: string(),
11817
+ chosen: string(),
11818
+ running: string(),
11819
+ format: string()
11820
+ });
11821
+ var PipelineValidationIssueSchema = object({
11822
+ addonId: string(),
11823
+ kind: _enum(["unknown-addon", "no-format-build"]),
11824
+ detail: string()
11825
+ });
11826
+ var PipelineValidationResultSchema = object({
11827
+ ok: boolean(),
11828
+ issues: array(PipelineValidationIssueSchema).readonly(),
11829
+ substitutions: array(ModelSubstitutionSchema).readonly(),
11830
+ /** The node's `currentEngine.format` this validation ran against. */
11831
+ format: string()
11832
+ });
11778
11833
  var ReferenceImageEntrySchema = object({
11779
11834
  filename: string(),
11780
11835
  stepIds: array(string()).readonly().optional()
@@ -11845,7 +11900,7 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
11845
11900
  })) }), object({ success: literal(true) }), {
11846
11901
  kind: "mutation",
11847
11902
  auth: "admin"
11848
- }), method(_void(), PipelineSchemaSchema), method(_void(), array(PipelineDefaultStepSchema).readonly().nullable()), method(_void(), PipelineConfigBridge), method(_void(), ConfigUISchemaBridge), method(_void(), array(PipelineTemplateSchema$1).readonly()), method(object({
11903
+ }), 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({
11849
11904
  name: string(),
11850
11905
  steps: array(PipelineTemplateStepSchema).readonly(),
11851
11906
  engine: PipelineEngineChoiceSchema
@@ -12138,6 +12193,13 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
12138
12193
  kind: literal("remote-restream"),
12139
12194
  /** The camera's source-owner node (slice 1: always the hub). */
12140
12195
  ownerNodeId: string(),
12196
+ /**
12197
+ * The owner's LAN-reachable host, resolved by the orchestrator from the
12198
+ * per-node `reachableHost` override (Cluster UI). When present the runner
12199
+ * dials THIS host for the owner's restream, in preference to the
12200
+ * `CAMSTACK_HUB_URL`-derived default. Absent → auto-detect fallback.
12201
+ */
12202
+ ownerReachableHost: string().optional(),
12141
12203
  /** Operator override for the owner host the runner dials. */
12142
12204
  hubHostnameOverride: string().optional()
12143
12205
  })]).describe("Per-camera frame-source mode for the runner (P2c)");
@@ -12146,13 +12208,11 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
12146
12208
  * specific runner instance via `attachCamera`. Carries everything the
12147
12209
  * runner needs to subscribe to the local broker and execute inference.
12148
12210
  *
12149
- * Stateless-pipeline model: the full pipeline content (`engine`, `steps`,
12150
- * optional `audio`) travels with the attach payload. The runner keeps it
12151
- * in RAM for the lifetime of the attach — on rebalance, edit, or
12152
- * restart the orchestrator re-sends the latest snapshot.
12153
- *
12154
- * `engine`/`steps`/`audio` are optional during the additive migration
12155
- * window; once orchestrator + UI are migrated they become required.
12211
+ * Stateless-pipeline model: the pipeline content (`steps`, optional
12212
+ * `audio`) travels with the attach payload. The runner keeps it in RAM
12213
+ * for the lifetime of the attach — on rebalance, edit, or restart the
12214
+ * orchestrator re-sends the latest snapshot. Engine is NOT carried: it is
12215
+ * node-local, resolved by the executing runner at dispatch time.
12156
12216
  */
12157
12217
  var RunnerCameraConfigSchema = object({
12158
12218
  deviceId: number(),
@@ -12203,14 +12263,11 @@ var RunnerCameraConfigSchema = object({
12203
12263
  */
12204
12264
  motionSources: MotionSourcesSchema.default(["analyzer"]),
12205
12265
  pipelineEnabled: boolean().default(true),
12206
- /** Engine choice for video steps (runtime+backend+format). */
12207
- engine: PipelineEngineChoiceSchema.optional(),
12208
12266
  /** Ordered tree of video steps. Absent → runner skips video detection. */
12209
12267
  steps: array(PipelineStepInputSchema).readonly().optional(),
12210
12268
  /** Audio classification branch. `enabled:false` disables, null skips. */
12211
12269
  audio: object({
12212
- engine: PipelineEngineChoiceSchema,
12213
- modelId: string(),
12270
+ modelId: string().optional(),
12214
12271
  enabled: boolean()
12215
12272
  }).nullable().optional(),
12216
12273
  /**
@@ -18073,11 +18130,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18073
18130
  timestamp: number()
18074
18131
  });
18075
18132
  var CameraPipelineConfigSchema = object({
18076
- engine: PipelineEngineChoiceSchema,
18133
+ engine: PipelineEngineChoiceSchema.optional(),
18077
18134
  steps: array(PipelineStepInputSchema).readonly(),
18078
18135
  audio: object({
18079
- engine: PipelineEngineChoiceSchema,
18080
- modelId: string(),
18136
+ engine: PipelineEngineChoiceSchema.optional(),
18137
+ modelId: string().optional(),
18081
18138
  enabled: boolean(),
18082
18139
  settings: record(string(), unknown()).readonly().optional()
18083
18140
  }).nullable().optional()
@@ -18092,7 +18149,7 @@ var PipelineTemplateSchema = object({
18092
18149
  });
18093
18150
  var AgentAddonConfigSchema = object({
18094
18151
  enabled: boolean(),
18095
- modelId: string(),
18152
+ modelId: string().optional(),
18096
18153
  settings: record(string(), unknown()).readonly()
18097
18154
  });
18098
18155
  var AgentPipelineSettingsSchema = object({
@@ -18107,7 +18164,15 @@ var AgentPipelineSettingsSchema = object({
18107
18164
  /** Node is eligible to run audio-analyzer sessions. */
18108
18165
  audio: boolean().optional(),
18109
18166
  /** Node is eligible to be the ingest / source-owner (serve the restream). */
18110
- ingest: boolean().optional()
18167
+ ingest: boolean().optional(),
18168
+ /**
18169
+ * Operator override for the LAN host a cross-node decoder dials to reach
18170
+ * THIS node's restream (Cluster UI). Absent → auto-detect: a remote runner
18171
+ * falls back to its `CAMSTACK_HUB_URL`-derived host (the Moleculer address
18172
+ * it already uses to reach the hub). Set this only when the auto-detected
18173
+ * address is wrong (multi-homed host, NAT, custom interface).
18174
+ */
18175
+ reachableHost: string().optional()
18111
18176
  });
18112
18177
  var CameraPipelineForAgentSchema = object({
18113
18178
  steps: array(PipelineStepInputSchema).readonly(),
@@ -18213,6 +18278,15 @@ var GlobalMetricsSchema = object({
18213
18278
  * capability providers.
18214
18279
  */
18215
18280
  var CapabilityBindingsSchema = record(string(), string());
18281
+ /**
18282
+ * The cluster's single camera-source owner (`clusterRoles.ingestNode`) plus
18283
+ * its LAN-reachable host, if one is registered. See `getIngestOwner`.
18284
+ */
18285
+ var IngestOwnerSchema = object({
18286
+ ownerNodeId: string(),
18287
+ reachableHost: string().optional(),
18288
+ configIssue: string().optional()
18289
+ });
18216
18290
  /** Source block — always present; derives from the stream catalog. */
18217
18291
  var CameraSourceStatusSchema = object({ streams: array(object({
18218
18292
  camStreamId: string(),
@@ -18227,6 +18301,14 @@ var CameraAssignmentStatusSchema = object({
18227
18301
  detectionNodeId: string().nullable(),
18228
18302
  decoderNodeId: string().nullable(),
18229
18303
  audioNodeId: string().nullable(),
18304
+ /**
18305
+ * The node that OWNS this camera's physical source pull (dials the RTSP and
18306
+ * hosts the broker/restream) — the cluster ingest owner today
18307
+ * (`clusterRoles.ingestNode`), per-camera once source assignment lands. Lets
18308
+ * the UI show WHERE a camera is sourced without SSH/logs, and is the node the
18309
+ * broker block below was read from (pinned). Nullable only pre-wiring.
18310
+ */
18311
+ sourceNodeId: string().nullable(),
18230
18312
  pinned: object({
18231
18313
  detection: boolean(),
18232
18314
  decoder: boolean(),
@@ -18359,7 +18441,7 @@ method(object({
18359
18441
  }), object({ success: literal(true) }), {
18360
18442
  kind: "mutation",
18361
18443
  auth: "admin"
18362
- }), method(object({
18444
+ }), method(_void(), IngestOwnerSchema), method(object({
18363
18445
  deviceId: number(),
18364
18446
  nodeId: string()
18365
18447
  }), _void(), {
@@ -18428,6 +18510,12 @@ method(object({
18428
18510
  }), object({ success: literal(true) }), {
18429
18511
  kind: "mutation",
18430
18512
  auth: "admin"
18513
+ }), method(object({
18514
+ agentNodeId: string(),
18515
+ reachableHost: string().nullable()
18516
+ }), object({ success: literal(true) }), {
18517
+ kind: "mutation",
18518
+ auth: "admin"
18431
18519
  }), method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()), method(object({
18432
18520
  deviceId: number(),
18433
18521
  addonId: string(),
@@ -18472,22 +18560,6 @@ method(object({
18472
18560
  kind: "mutation",
18473
18561
  auth: "admin"
18474
18562
  });
18475
- var RegisteredStreamSchema = object({
18476
- streamId: string(),
18477
- label: string().optional(),
18478
- codec: string(),
18479
- type: _enum(["video", "audio"]),
18480
- sourceUrl: string()
18481
- });
18482
- var ExposedResourceSchema = object({
18483
- streamId: string(),
18484
- format: string(),
18485
- value: string()
18486
- });
18487
- method(object({
18488
- deviceId: number(),
18489
- streams: array(RegisteredStreamSchema).readonly()
18490
- }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), array(ExposedResourceSchema).readonly());
18491
18563
  /**
18492
18564
  * Query filter for settings-store collections.
18493
18565
  */
@@ -18640,9 +18712,9 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
18640
18712
  /**
18641
18713
  * A single device snapshot returned as base64 JPEG/PNG.
18642
18714
  *
18643
- * Shared with the `snapshot-provider` collection cap the orchestrator
18644
- * receives the same shape from each native provider and from the
18645
- * broker-based fallback.
18715
+ * The `SnapshotAddon` wrapper returns this shape whether the frame came from
18716
+ * the device-native provider (onboard capture) or from the stream-broker
18717
+ * prebuffer fallback.
18646
18718
  */
18647
18719
  var SnapshotImageSchema = object({
18648
18720
  base64: string(),
@@ -18717,10 +18789,6 @@ var snapshotCapability = {
18717
18789
  kind: "poll"
18718
18790
  }
18719
18791
  };
18720
- method(object({ deviceId: number() }), boolean()), method(object({
18721
- deviceId: number(),
18722
- streamId: string().optional()
18723
- }), SnapshotImageSchema.nullable());
18724
18792
  /**
18725
18793
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
18726
18794
  * providers (OIDC, SAML, magic-link, …) mint an HMAC-signed token
@@ -19081,9 +19149,10 @@ method(object({
19081
19149
  auth: "admin"
19082
19150
  });
19083
19151
  /**
19084
- * Optional client-side hints sent at session creation to help the
19085
- * provider pick the best native source. All fields are optional —
19086
- * a viewer that knows nothing still gets a sane default.
19152
+ * Optional client-side hints sent at session creation to help the provider
19153
+ * pick the best native source. All fields optional — a viewer that knows
19154
+ * nothing still gets a sane default. (Relocated from the retired `webrtc`
19155
+ * collection cap; this `webrtc-session` cap is the live signaling surface.)
19087
19156
  */
19088
19157
  var webrtcClientHintsSchema = object({
19089
19158
  viewportWidth: number().int().positive().optional(),
@@ -19094,22 +19163,6 @@ var webrtcClientHintsSchema = object({
19094
19163
  /** Hard tier override; takes precedence over scoring when registered. */
19095
19164
  prefersTier: string().optional()
19096
19165
  }).partial();
19097
- method(object({
19098
- streamId: string(),
19099
- sdpOffer: string()
19100
- }), string(), { kind: "mutation" }), method(object({ streamId: string() }), boolean()), method(object({
19101
- streamId: string(),
19102
- codec: string()
19103
- }), _void(), { kind: "mutation" }), method(object({ streamId: string() }), _void(), { kind: "mutation" }), method(object({
19104
- streamId: string(),
19105
- hints: webrtcClientHintsSchema.optional()
19106
- }), object({
19107
- sessionId: string(),
19108
- sdpOffer: string()
19109
- }), { kind: "mutation" }), method(object({
19110
- sessionId: string(),
19111
- sdpAnswer: string()
19112
- }), _void(), { kind: "mutation" }), method(object({ sessionId: string() }), _void(), { kind: "mutation" }), method(object({ streamId: string() }), boolean());
19113
19166
  /**
19114
19167
  * Discriminated target for a WebRTC session. The client sends this
19115
19168
  * structured object instead of building / parsing brokerId strings;
@@ -24499,6 +24552,12 @@ Object.freeze({
24499
24552
  addonId: null,
24500
24553
  access: "create"
24501
24554
  },
24555
+ "pipelineExecutor.validatePipeline": {
24556
+ capName: "pipeline-executor",
24557
+ capScope: "system",
24558
+ addonId: null,
24559
+ access: "view"
24560
+ },
24502
24561
  "pipelineOrchestrator.assignAudio": {
24503
24562
  capName: "pipeline-orchestrator",
24504
24563
  capScope: "system",
@@ -24607,6 +24666,12 @@ Object.freeze({
24607
24666
  addonId: null,
24608
24667
  access: "view"
24609
24668
  },
24669
+ "pipelineOrchestrator.getIngestOwner": {
24670
+ capName: "pipeline-orchestrator",
24671
+ capScope: "system",
24672
+ addonId: null,
24673
+ access: "view"
24674
+ },
24610
24675
  "pipelineOrchestrator.getPipelineAssignment": {
24611
24676
  capName: "pipeline-orchestrator",
24612
24677
  capScope: "system",
@@ -24679,6 +24744,12 @@ Object.freeze({
24679
24744
  addonId: null,
24680
24745
  access: "create"
24681
24746
  },
24747
+ "pipelineOrchestrator.setAgentReachableHost": {
24748
+ capName: "pipeline-orchestrator",
24749
+ capScope: "system",
24750
+ addonId: null,
24751
+ access: "create"
24752
+ },
24682
24753
  "pipelineOrchestrator.setCameraPipelineForAgent": {
24683
24754
  capName: "pipeline-orchestrator",
24684
24755
  capScope: "system",
@@ -25009,24 +25080,6 @@ Object.freeze({
25009
25080
  addonId: null,
25010
25081
  access: "create"
25011
25082
  },
25012
- "restreamer.getExposedResources": {
25013
- capName: "restreamer",
25014
- capScope: "system",
25015
- addonId: null,
25016
- access: "view"
25017
- },
25018
- "restreamer.registerDevice": {
25019
- capName: "restreamer",
25020
- capScope: "system",
25021
- addonId: null,
25022
- access: "create"
25023
- },
25024
- "restreamer.unregisterDevice": {
25025
- capName: "restreamer",
25026
- capScope: "system",
25027
- addonId: null,
25028
- access: "delete"
25029
- },
25030
25083
  "scriptRunner.run": {
25031
25084
  capName: "script-runner",
25032
25085
  capScope: "device",
@@ -25129,18 +25182,6 @@ Object.freeze({
25129
25182
  addonId: null,
25130
25183
  access: "create"
25131
25184
  },
25132
- "snapshotProvider.getSnapshot": {
25133
- capName: "snapshot-provider",
25134
- capScope: "system",
25135
- addonId: null,
25136
- access: "view"
25137
- },
25138
- "snapshotProvider.supportsDevice": {
25139
- capName: "snapshot-provider",
25140
- capScope: "system",
25141
- addonId: null,
25142
- access: "view"
25143
- },
25144
25185
  "ssoBridge.signBridgeToken": {
25145
25186
  capName: "sso-bridge",
25146
25187
  capScope: "system",
@@ -25567,30 +25608,6 @@ Object.freeze({
25567
25608
  addonId: null,
25568
25609
  access: "view"
25569
25610
  },
25570
- "streamingEngine.getStreamUrl": {
25571
- capName: "streaming-engine",
25572
- capScope: "system",
25573
- addonId: null,
25574
- access: "view"
25575
- },
25576
- "streamingEngine.listStreams": {
25577
- capName: "streaming-engine",
25578
- capScope: "system",
25579
- addonId: null,
25580
- access: "view"
25581
- },
25582
- "streamingEngine.registerStream": {
25583
- capName: "streaming-engine",
25584
- capScope: "system",
25585
- addonId: null,
25586
- access: "create"
25587
- },
25588
- "streamingEngine.unregisterStream": {
25589
- capName: "streaming-engine",
25590
- capScope: "system",
25591
- addonId: null,
25592
- access: "delete"
25593
- },
25594
25611
  "streamParams.getConfigSchema": {
25595
25612
  capName: "stream-params",
25596
25613
  capScope: "device",
@@ -25957,54 +25974,6 @@ Object.freeze({
25957
25974
  addonId: null,
25958
25975
  access: "create"
25959
25976
  },
25960
- "webrtc.closeSession": {
25961
- capName: "webrtc",
25962
- capScope: "system",
25963
- addonId: null,
25964
- access: "create"
25965
- },
25966
- "webrtc.createSession": {
25967
- capName: "webrtc",
25968
- capScope: "system",
25969
- addonId: null,
25970
- access: "create"
25971
- },
25972
- "webrtc.handleAnswer": {
25973
- capName: "webrtc",
25974
- capScope: "system",
25975
- addonId: null,
25976
- access: "create"
25977
- },
25978
- "webrtc.handleOffer": {
25979
- capName: "webrtc",
25980
- capScope: "system",
25981
- addonId: null,
25982
- access: "create"
25983
- },
25984
- "webrtc.hasAdaptiveBitrate": {
25985
- capName: "webrtc",
25986
- capScope: "system",
25987
- addonId: null,
25988
- access: "view"
25989
- },
25990
- "webrtc.registerStream": {
25991
- capName: "webrtc",
25992
- capScope: "system",
25993
- addonId: null,
25994
- access: "create"
25995
- },
25996
- "webrtc.supportsStream": {
25997
- capName: "webrtc",
25998
- capScope: "system",
25999
- addonId: null,
26000
- access: "view"
26001
- },
26002
- "webrtc.unregisterStream": {
26003
- capName: "webrtc",
26004
- capScope: "system",
26005
- addonId: null,
26006
- access: "delete"
26007
- },
26008
25977
  "webrtcSession.addIceCandidate": {
26009
25978
  capName: "webrtc-session",
26010
25979
  capScope: "device",
@@ -26534,17 +26503,21 @@ var AmcrestCgiClient = class AmcrestCgiClient {
26534
26503
  } catch {
26535
26504
  return [];
26536
26505
  }
26537
- const out = /* @__PURE__ */ new Map();
26538
26506
  const ch = channel - 1;
26507
+ const names = /* @__PURE__ */ new Map();
26508
+ const enabled = /* @__PURE__ */ new Set();
26539
26509
  for (const [key, value] of t) {
26540
- const m = new RegExp(`^PtzPreset\\[${ch}\\]\\[(\\d+)\\]\\.Name$`).exec(key);
26541
- if (!m) continue;
26542
- const idx = Number.parseInt(m[1], 10);
26543
- out.set(idx, value);
26510
+ const nameMatch = new RegExp(`^PtzPreset\\[${ch}\\]\\[(\\d+)\\]\\.Name$`).exec(key);
26511
+ if (nameMatch) {
26512
+ names.set(Number.parseInt(nameMatch[1], 10), value);
26513
+ continue;
26514
+ }
26515
+ const enableMatch = new RegExp(`^PtzPreset\\[${ch}\\]\\[(\\d+)\\]\\.Enable$`).exec(key);
26516
+ if (enableMatch && value.trim().toLowerCase() === "true") enabled.add(Number.parseInt(enableMatch[1], 10));
26544
26517
  }
26545
- return [...out.entries()].toSorted((a, b) => a[0] - b[0]).map(([id, name]) => ({
26546
- id,
26547
- name: name || `Preset ${id}`
26518
+ return [...enabled].toSorted((a, b) => a - b).map((idx) => ({
26519
+ id: idx + 1,
26520
+ name: names.get(idx) || `Preset ${idx + 1}`
26548
26521
  }));
26549
26522
  }
26550
26523
  };
@@ -27178,9 +27151,17 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
27178
27151
  },
27179
27152
  savePreset: async ({ deviceId, presetId, name }) => {
27180
27153
  if (deviceId !== this.id) return;
27181
- const id = Number.parseInt(presetId, 10);
27182
- if (!Number.isFinite(id)) return;
27183
- await this.ensureClient().ptzSetPreset(this.channel, id);
27154
+ const client = this.ensureClient();
27155
+ const requested = Number.parseInt(presetId, 10);
27156
+ const DAHUA_MAX_PRESETS = 25;
27157
+ let slot;
27158
+ if (Number.isInteger(requested) && requested >= 1 && requested <= DAHUA_MAX_PRESETS) slot = requested;
27159
+ else {
27160
+ const used = new Set((await client.getPtzPresets(this.channel)).map((p) => p.id));
27161
+ slot = 1;
27162
+ while (used.has(slot)) slot += 1;
27163
+ }
27164
+ await client.ptzSetPreset(this.channel, slot);
27184
27165
  },
27185
27166
  deletePreset: async ({ deviceId, presetId }) => {
27186
27167
  if (deviceId !== this.id) return;
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,28 @@ 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 / reduced-input variant)
6936
+ * — the slot the future performance variants plug into.
6937
+ */
6938
+ optimization: _enum(["standard", "fast"]).optional()
6939
+ });
6906
6940
  var ModelCatalogEntrySchema = object({
6907
6941
  id: string(),
6908
6942
  name: string(),
@@ -6932,7 +6966,43 @@ var ModelCatalogEntrySchema = object({
6932
6966
  * Auxiliary files required at runtime (labels JSON, charset dict, etc.).
6933
6967
  * Downloaded into the same modelsDir alongside the model file.
6934
6968
  */
6935
- extraFiles: array(ModelExtraFileSchema).readonly().optional()
6969
+ extraFiles: array(ModelExtraFileSchema).readonly().optional(),
6970
+ /**
6971
+ * LEGACY entry — retained in the catalog so a persisted operator selection
6972
+ * still RESOLVES (and can be re-activated), but hidden from the selectable
6973
+ * model list and excluded from the auto format-default pick. Set on the
6974
+ * superseded / consolidated models (older lineages, redundant fp16 IRs) so
6975
+ * the active lineup stays the coherent curated ladder without deleting a
6976
+ * model anyone may still be pinned to. `resolveModelForFormat` keeps honoring
6977
+ * an explicit legacy id that has a build for the node's format.
6978
+ */
6979
+ legacy: boolean().optional(),
6980
+ /**
6981
+ * Measured quality/latency metadata — populated from the benchmark addon on
6982
+ * the real node classes. Absent = not yet measured (most entries today; the
6983
+ * catalog historically carried only `sizeMB`, a poor cross-architecture
6984
+ * speed proxy). `p95LatencyMs` is keyed by node class (e.g. `n100`, `mac`).
6985
+ */
6986
+ metrics: object({
6987
+ map50: number().optional(),
6988
+ p95LatencyMs: record(string(), number()).optional()
6989
+ }).optional(),
6990
+ /**
6991
+ * SPDX-ish license id of the model weights (e.g. `AGPL-3.0` for Ultralytics
6992
+ * YOLO26, `GPL-3.0` for YOLOv9, `Apache-2.0` for D-FINE/RF-DETR). Matters for
6993
+ * the retraining addon and any future commercial distribution.
6994
+ */
6995
+ license: string().optional(),
6996
+ /**
6997
+ * Variant-selector grouping. The UI groups models by `family` + `tier` and
6998
+ * offers `precision` / `optimization` as variant axes WITHIN a tier — so all
6999
+ * of a family's sizes and quantizations collapse into one grouped picker
7000
+ * instead of a flat list of `yolo26s`, `yolo26s-int8`, … Absent ⇒ ungrouped
7001
+ * (legacy / custom models) — never shown in the grouped selector. The flat
7002
+ * `id` stays the source of truth for resolution/download/persistence; grouping
7003
+ * is a presentation overlay resolved back to an `id`.
7004
+ */
7005
+ group: ModelVariantGroupSchema.optional()
6936
7006
  });
6937
7007
  var ConvertTargetSchema = discriminatedUnion("format", [object({
6938
7008
  format: literal("openvino"),
@@ -6993,8 +7063,8 @@ var RecordingModeSchema = _enum([
6993
7063
  "onAudioThreshold"
6994
7064
  ]);
6995
7065
  /**
6996
- * First-class, authoritative per-camera storage mode — the netta choice the UI
6997
- * reads directly (never inferred from `rules`):
7066
+ * First-class, authoritative per-camera storage mode — the explicit choice the
7067
+ * UI reads directly (never inferred from `rules`):
6998
7068
  * - `off` — not recording.
6999
7069
  * - `events` — record only around triggers (motion / audio threshold),
7000
7070
  * with pre/post-buffer.
@@ -9181,26 +9251,13 @@ onBrightnessChanged: { data: object({
9181
9251
  */
9182
9252
  runtimeState: BrightnessStatusSchema
9183
9253
  };
9254
+ /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
9184
9255
  var StreamFormatSchema = _enum([
9185
9256
  "webrtc",
9186
9257
  "hls",
9187
9258
  "mjpeg",
9188
9259
  "rtsp"
9189
9260
  ]);
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
9261
  var RtspRestreamEntrySchema = object({
9205
9262
  brokerId: string(),
9206
9263
  url: string(),
@@ -10068,37 +10125,7 @@ var consumablesCapability = {
10068
10125
  scope: "device",
10069
10126
  deviceNative: true,
10070
10127
  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
- ],
10128
+ deviceTypes: Object.values(DeviceType),
10102
10129
  deviceConfig: { ui: {
10103
10130
  kind: "widget",
10104
10131
  widgetId: "host/consumables-panel",
@@ -11691,7 +11718,6 @@ var PipelineDefaultStepSchema = lazy(() => object({
11691
11718
  enabled: boolean(),
11692
11719
  modelId: string(),
11693
11720
  children: array(PipelineDefaultStepSchema).readonly(),
11694
- engine: PipelineEngineChoiceSchema.optional(),
11695
11721
  group: string().optional(),
11696
11722
  settings: record(string(), unknown()).optional()
11697
11723
  }));
@@ -11716,7 +11742,9 @@ var PipelineModelOptionSchema = object({
11716
11742
  formats: record(string(), object({
11717
11743
  downloaded: boolean(),
11718
11744
  sizeMB: number()
11719
- }))
11745
+ })),
11746
+ group: ModelVariantGroupSchema.optional(),
11747
+ legacy: boolean().optional()
11720
11748
  });
11721
11749
  var ConfigFieldBridge = custom();
11722
11750
  var PipelineAddonSchemaSchema = object({
@@ -11767,15 +11795,42 @@ var EngineProvisioningSchema = object({
11767
11795
  ]),
11768
11796
  progress: number().optional(),
11769
11797
  error: string().optional(),
11770
- nextRetryAt: number().optional()
11798
+ nextRetryAt: number().optional(),
11799
+ /**
11800
+ * Gate A (config-correctness gate at engine change): human-readable
11801
+ * config issues surfaced EAGERLY when the node's engine changes — model
11802
+ * substitutions ("chose X, running Y") and zero-build steps ("no model
11803
+ * has a <format> build"). Additive/optional: informational only, never
11804
+ * enforced here — `assertEngineReady` (readiness) still gates inference.
11805
+ * Absent/empty when the node-default tree resolves cleanly.
11806
+ */
11807
+ configIssues: array(string()).optional()
11771
11808
  });
11772
11809
  var PipelineStepInputSchema = lazy(() => object({
11773
11810
  addonId: string(),
11774
- modelId: string(),
11811
+ modelId: string().optional(),
11775
11812
  enabled: boolean().default(true),
11776
11813
  children: array(PipelineStepInputSchema).optional(),
11777
11814
  settings: record(string(), unknown()).optional()
11778
11815
  }));
11816
+ var ModelSubstitutionSchema = object({
11817
+ addonId: string(),
11818
+ chosen: string(),
11819
+ running: string(),
11820
+ format: string()
11821
+ });
11822
+ var PipelineValidationIssueSchema = object({
11823
+ addonId: string(),
11824
+ kind: _enum(["unknown-addon", "no-format-build"]),
11825
+ detail: string()
11826
+ });
11827
+ var PipelineValidationResultSchema = object({
11828
+ ok: boolean(),
11829
+ issues: array(PipelineValidationIssueSchema).readonly(),
11830
+ substitutions: array(ModelSubstitutionSchema).readonly(),
11831
+ /** The node's `currentEngine.format` this validation ran against. */
11832
+ format: string()
11833
+ });
11779
11834
  var ReferenceImageEntrySchema = object({
11780
11835
  filename: string(),
11781
11836
  stepIds: array(string()).readonly().optional()
@@ -11846,7 +11901,7 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
11846
11901
  })) }), object({ success: literal(true) }), {
11847
11902
  kind: "mutation",
11848
11903
  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({
11904
+ }), 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
11905
  name: string(),
11851
11906
  steps: array(PipelineTemplateStepSchema).readonly(),
11852
11907
  engine: PipelineEngineChoiceSchema
@@ -12139,6 +12194,13 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
12139
12194
  kind: literal("remote-restream"),
12140
12195
  /** The camera's source-owner node (slice 1: always the hub). */
12141
12196
  ownerNodeId: string(),
12197
+ /**
12198
+ * The owner's LAN-reachable host, resolved by the orchestrator from the
12199
+ * per-node `reachableHost` override (Cluster UI). When present the runner
12200
+ * dials THIS host for the owner's restream, in preference to the
12201
+ * `CAMSTACK_HUB_URL`-derived default. Absent → auto-detect fallback.
12202
+ */
12203
+ ownerReachableHost: string().optional(),
12142
12204
  /** Operator override for the owner host the runner dials. */
12143
12205
  hubHostnameOverride: string().optional()
12144
12206
  })]).describe("Per-camera frame-source mode for the runner (P2c)");
@@ -12147,13 +12209,11 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
12147
12209
  * specific runner instance via `attachCamera`. Carries everything the
12148
12210
  * runner needs to subscribe to the local broker and execute inference.
12149
12211
  *
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.
12212
+ * Stateless-pipeline model: the pipeline content (`steps`, optional
12213
+ * `audio`) travels with the attach payload. The runner keeps it in RAM
12214
+ * for the lifetime of the attach — on rebalance, edit, or restart the
12215
+ * orchestrator re-sends the latest snapshot. Engine is NOT carried: it is
12216
+ * node-local, resolved by the executing runner at dispatch time.
12157
12217
  */
12158
12218
  var RunnerCameraConfigSchema = object({
12159
12219
  deviceId: number(),
@@ -12204,14 +12264,11 @@ var RunnerCameraConfigSchema = object({
12204
12264
  */
12205
12265
  motionSources: MotionSourcesSchema.default(["analyzer"]),
12206
12266
  pipelineEnabled: boolean().default(true),
12207
- /** Engine choice for video steps (runtime+backend+format). */
12208
- engine: PipelineEngineChoiceSchema.optional(),
12209
12267
  /** Ordered tree of video steps. Absent → runner skips video detection. */
12210
12268
  steps: array(PipelineStepInputSchema).readonly().optional(),
12211
12269
  /** Audio classification branch. `enabled:false` disables, null skips. */
12212
12270
  audio: object({
12213
- engine: PipelineEngineChoiceSchema,
12214
- modelId: string(),
12271
+ modelId: string().optional(),
12215
12272
  enabled: boolean()
12216
12273
  }).nullable().optional(),
12217
12274
  /**
@@ -18074,11 +18131,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18074
18131
  timestamp: number()
18075
18132
  });
18076
18133
  var CameraPipelineConfigSchema = object({
18077
- engine: PipelineEngineChoiceSchema,
18134
+ engine: PipelineEngineChoiceSchema.optional(),
18078
18135
  steps: array(PipelineStepInputSchema).readonly(),
18079
18136
  audio: object({
18080
- engine: PipelineEngineChoiceSchema,
18081
- modelId: string(),
18137
+ engine: PipelineEngineChoiceSchema.optional(),
18138
+ modelId: string().optional(),
18082
18139
  enabled: boolean(),
18083
18140
  settings: record(string(), unknown()).readonly().optional()
18084
18141
  }).nullable().optional()
@@ -18093,7 +18150,7 @@ var PipelineTemplateSchema = object({
18093
18150
  });
18094
18151
  var AgentAddonConfigSchema = object({
18095
18152
  enabled: boolean(),
18096
- modelId: string(),
18153
+ modelId: string().optional(),
18097
18154
  settings: record(string(), unknown()).readonly()
18098
18155
  });
18099
18156
  var AgentPipelineSettingsSchema = object({
@@ -18108,7 +18165,15 @@ var AgentPipelineSettingsSchema = object({
18108
18165
  /** Node is eligible to run audio-analyzer sessions. */
18109
18166
  audio: boolean().optional(),
18110
18167
  /** Node is eligible to be the ingest / source-owner (serve the restream). */
18111
- ingest: boolean().optional()
18168
+ ingest: boolean().optional(),
18169
+ /**
18170
+ * Operator override for the LAN host a cross-node decoder dials to reach
18171
+ * THIS node's restream (Cluster UI). Absent → auto-detect: a remote runner
18172
+ * falls back to its `CAMSTACK_HUB_URL`-derived host (the Moleculer address
18173
+ * it already uses to reach the hub). Set this only when the auto-detected
18174
+ * address is wrong (multi-homed host, NAT, custom interface).
18175
+ */
18176
+ reachableHost: string().optional()
18112
18177
  });
18113
18178
  var CameraPipelineForAgentSchema = object({
18114
18179
  steps: array(PipelineStepInputSchema).readonly(),
@@ -18214,6 +18279,15 @@ var GlobalMetricsSchema = object({
18214
18279
  * capability providers.
18215
18280
  */
18216
18281
  var CapabilityBindingsSchema = record(string(), string());
18282
+ /**
18283
+ * The cluster's single camera-source owner (`clusterRoles.ingestNode`) plus
18284
+ * its LAN-reachable host, if one is registered. See `getIngestOwner`.
18285
+ */
18286
+ var IngestOwnerSchema = object({
18287
+ ownerNodeId: string(),
18288
+ reachableHost: string().optional(),
18289
+ configIssue: string().optional()
18290
+ });
18217
18291
  /** Source block — always present; derives from the stream catalog. */
18218
18292
  var CameraSourceStatusSchema = object({ streams: array(object({
18219
18293
  camStreamId: string(),
@@ -18228,6 +18302,14 @@ var CameraAssignmentStatusSchema = object({
18228
18302
  detectionNodeId: string().nullable(),
18229
18303
  decoderNodeId: string().nullable(),
18230
18304
  audioNodeId: string().nullable(),
18305
+ /**
18306
+ * The node that OWNS this camera's physical source pull (dials the RTSP and
18307
+ * hosts the broker/restream) — the cluster ingest owner today
18308
+ * (`clusterRoles.ingestNode`), per-camera once source assignment lands. Lets
18309
+ * the UI show WHERE a camera is sourced without SSH/logs, and is the node the
18310
+ * broker block below was read from (pinned). Nullable only pre-wiring.
18311
+ */
18312
+ sourceNodeId: string().nullable(),
18231
18313
  pinned: object({
18232
18314
  detection: boolean(),
18233
18315
  decoder: boolean(),
@@ -18360,7 +18442,7 @@ method(object({
18360
18442
  }), object({ success: literal(true) }), {
18361
18443
  kind: "mutation",
18362
18444
  auth: "admin"
18363
- }), method(object({
18445
+ }), method(_void(), IngestOwnerSchema), method(object({
18364
18446
  deviceId: number(),
18365
18447
  nodeId: string()
18366
18448
  }), _void(), {
@@ -18429,6 +18511,12 @@ method(object({
18429
18511
  }), object({ success: literal(true) }), {
18430
18512
  kind: "mutation",
18431
18513
  auth: "admin"
18514
+ }), method(object({
18515
+ agentNodeId: string(),
18516
+ reachableHost: string().nullable()
18517
+ }), object({ success: literal(true) }), {
18518
+ kind: "mutation",
18519
+ auth: "admin"
18432
18520
  }), method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()), method(object({
18433
18521
  deviceId: number(),
18434
18522
  addonId: string(),
@@ -18473,22 +18561,6 @@ method(object({
18473
18561
  kind: "mutation",
18474
18562
  auth: "admin"
18475
18563
  });
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
18564
  /**
18493
18565
  * Query filter for settings-store collections.
18494
18566
  */
@@ -18641,9 +18713,9 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
18641
18713
  /**
18642
18714
  * A single device snapshot returned as base64 JPEG/PNG.
18643
18715
  *
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.
18716
+ * The `SnapshotAddon` wrapper returns this shape whether the frame came from
18717
+ * the device-native provider (onboard capture) or from the stream-broker
18718
+ * prebuffer fallback.
18647
18719
  */
18648
18720
  var SnapshotImageSchema = object({
18649
18721
  base64: string(),
@@ -18718,10 +18790,6 @@ var snapshotCapability = {
18718
18790
  kind: "poll"
18719
18791
  }
18720
18792
  };
18721
- method(object({ deviceId: number() }), boolean()), method(object({
18722
- deviceId: number(),
18723
- streamId: string().optional()
18724
- }), SnapshotImageSchema.nullable());
18725
18793
  /**
18726
18794
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
18727
18795
  * providers (OIDC, SAML, magic-link, …) mint an HMAC-signed token
@@ -19082,9 +19150,10 @@ method(object({
19082
19150
  auth: "admin"
19083
19151
  });
19084
19152
  /**
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.
19153
+ * Optional client-side hints sent at session creation to help the provider
19154
+ * pick the best native source. All fields optional — a viewer that knows
19155
+ * nothing still gets a sane default. (Relocated from the retired `webrtc`
19156
+ * collection cap; this `webrtc-session` cap is the live signaling surface.)
19088
19157
  */
19089
19158
  var webrtcClientHintsSchema = object({
19090
19159
  viewportWidth: number().int().positive().optional(),
@@ -19095,22 +19164,6 @@ var webrtcClientHintsSchema = object({
19095
19164
  /** Hard tier override; takes precedence over scoring when registered. */
19096
19165
  prefersTier: string().optional()
19097
19166
  }).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
19167
  /**
19115
19168
  * Discriminated target for a WebRTC session. The client sends this
19116
19169
  * structured object instead of building / parsing brokerId strings;
@@ -24500,6 +24553,12 @@ Object.freeze({
24500
24553
  addonId: null,
24501
24554
  access: "create"
24502
24555
  },
24556
+ "pipelineExecutor.validatePipeline": {
24557
+ capName: "pipeline-executor",
24558
+ capScope: "system",
24559
+ addonId: null,
24560
+ access: "view"
24561
+ },
24503
24562
  "pipelineOrchestrator.assignAudio": {
24504
24563
  capName: "pipeline-orchestrator",
24505
24564
  capScope: "system",
@@ -24608,6 +24667,12 @@ Object.freeze({
24608
24667
  addonId: null,
24609
24668
  access: "view"
24610
24669
  },
24670
+ "pipelineOrchestrator.getIngestOwner": {
24671
+ capName: "pipeline-orchestrator",
24672
+ capScope: "system",
24673
+ addonId: null,
24674
+ access: "view"
24675
+ },
24611
24676
  "pipelineOrchestrator.getPipelineAssignment": {
24612
24677
  capName: "pipeline-orchestrator",
24613
24678
  capScope: "system",
@@ -24680,6 +24745,12 @@ Object.freeze({
24680
24745
  addonId: null,
24681
24746
  access: "create"
24682
24747
  },
24748
+ "pipelineOrchestrator.setAgentReachableHost": {
24749
+ capName: "pipeline-orchestrator",
24750
+ capScope: "system",
24751
+ addonId: null,
24752
+ access: "create"
24753
+ },
24683
24754
  "pipelineOrchestrator.setCameraPipelineForAgent": {
24684
24755
  capName: "pipeline-orchestrator",
24685
24756
  capScope: "system",
@@ -25010,24 +25081,6 @@ Object.freeze({
25010
25081
  addonId: null,
25011
25082
  access: "create"
25012
25083
  },
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
25084
  "scriptRunner.run": {
25032
25085
  capName: "script-runner",
25033
25086
  capScope: "device",
@@ -25130,18 +25183,6 @@ Object.freeze({
25130
25183
  addonId: null,
25131
25184
  access: "create"
25132
25185
  },
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
25186
  "ssoBridge.signBridgeToken": {
25146
25187
  capName: "sso-bridge",
25147
25188
  capScope: "system",
@@ -25568,30 +25609,6 @@ Object.freeze({
25568
25609
  addonId: null,
25569
25610
  access: "view"
25570
25611
  },
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
25612
  "streamParams.getConfigSchema": {
25596
25613
  capName: "stream-params",
25597
25614
  capScope: "device",
@@ -25958,54 +25975,6 @@ Object.freeze({
25958
25975
  addonId: null,
25959
25976
  access: "create"
25960
25977
  },
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
25978
  "webrtcSession.addIceCandidate": {
26010
25979
  capName: "webrtc-session",
26011
25980
  capScope: "device",
@@ -26535,17 +26504,21 @@ var AmcrestCgiClient = class AmcrestCgiClient {
26535
26504
  } catch {
26536
26505
  return [];
26537
26506
  }
26538
- const out = /* @__PURE__ */ new Map();
26539
26507
  const ch = channel - 1;
26508
+ const names = /* @__PURE__ */ new Map();
26509
+ const enabled = /* @__PURE__ */ new Set();
26540
26510
  for (const [key, value] of t) {
26541
- const m = new RegExp(`^PtzPreset\\[${ch}\\]\\[(\\d+)\\]\\.Name$`).exec(key);
26542
- if (!m) continue;
26543
- const idx = Number.parseInt(m[1], 10);
26544
- out.set(idx, value);
26511
+ const nameMatch = new RegExp(`^PtzPreset\\[${ch}\\]\\[(\\d+)\\]\\.Name$`).exec(key);
26512
+ if (nameMatch) {
26513
+ names.set(Number.parseInt(nameMatch[1], 10), value);
26514
+ continue;
26515
+ }
26516
+ const enableMatch = new RegExp(`^PtzPreset\\[${ch}\\]\\[(\\d+)\\]\\.Enable$`).exec(key);
26517
+ if (enableMatch && value.trim().toLowerCase() === "true") enabled.add(Number.parseInt(enableMatch[1], 10));
26545
26518
  }
26546
- return [...out.entries()].toSorted((a, b) => a[0] - b[0]).map(([id, name]) => ({
26547
- id,
26548
- name: name || `Preset ${id}`
26519
+ return [...enabled].toSorted((a, b) => a - b).map((idx) => ({
26520
+ id: idx + 1,
26521
+ name: names.get(idx) || `Preset ${idx + 1}`
26549
26522
  }));
26550
26523
  }
26551
26524
  };
@@ -27179,9 +27152,17 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
27179
27152
  },
27180
27153
  savePreset: async ({ deviceId, presetId, name }) => {
27181
27154
  if (deviceId !== this.id) return;
27182
- const id = Number.parseInt(presetId, 10);
27183
- if (!Number.isFinite(id)) return;
27184
- await this.ensureClient().ptzSetPreset(this.channel, id);
27155
+ const client = this.ensureClient();
27156
+ const requested = Number.parseInt(presetId, 10);
27157
+ const DAHUA_MAX_PRESETS = 25;
27158
+ let slot;
27159
+ if (Number.isInteger(requested) && requested >= 1 && requested <= DAHUA_MAX_PRESETS) slot = requested;
27160
+ else {
27161
+ const used = new Set((await client.getPtzPresets(this.channel)).map((p) => p.id));
27162
+ slot = 1;
27163
+ while (used.has(slot)) slot += 1;
27164
+ }
27165
+ await client.ptzSetPreset(this.channel, slot);
27185
27166
  },
27186
27167
  deletePreset: async ({ deviceId, presetId }) => {
27187
27168
  if (deviceId !== this.id) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-amcrest",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Amcrest/Dahua camera device provider addon for CamStack — Dahua CGI over HTTP(S) with digest auth (snapshot, RTSP catalog, PTZ, image/day-night config)",
5
5
  "keywords": [
6
6
  "camstack",