Skip to main content

Getting Detections In

Condor is the AI that spots smoke and fire in the camera imagery. Its detections have to get into the platform — into Cosmos and SQL, onto the frontend, and out to ESRI's mapping. The Notification app is where that ingestion happens. Image detections arrive two different ways, one of which is the direction things are heading — and detection videos arrive by a third, separate path covered at the end.

The one idea to hold on to​

There are two ingestion lanes — a poll and an event-hub push — and a single flag, CondorEventSourceOptions.UseEventHub, decides which one actually processes. Both are always deployed (Azure Functions bindings are compile-time), and each simply early-returns when it isn't the selected mode:

// PollCondorApi (timer, every 30s)
if (_condorEventSourceOptions.UseEventHub) return; // event hub is on → poll stands down
// ConsumeCondorEventHubMessage (EventHubTrigger "condor_hub")
if (!_condorEventSourceOptions.UseEventHub) return; // event hub is off → hub trigger stands down

Both lanes funnel into the same ProcessCameraEvent, so the internal work is identical either way. What changes is where detections come from — and, crucially, whether the app forwards them to ESRI. Flip the switch:

🔌 Condor ingestion source
Poll · timer 30spulls Condor API
Push · condor_hubdeployed · skips
Notification app · ProcessCameraEventCosmos event · SQL event + COCO · SignalR → frontendinternal updates — always run
ESRI Velocity← published by the app

Poll mode (current): the app pulls the Condor API every 30s, updates internally, and also publishes fire localizations to ESRI Velocity.

  • Poll (current). A 30-second timer pulls the Condor API, enqueues events onto the AI-detection queue, and the dequeue handler processes them. In this mode the app also publishes fire localizations to ESRI Velocity.
  • Event hub (the target). Condor pushes detections onto condor_hub; the app consumes them and updates only internally.

Why event-hub mode stops publishing to ESRI​

This is the subtle part, and it's explicit in the code. When the event hub is the source, ESRI Velocity subscribes to that same hub through its own consumer group — it gets detections directly, without the Notification app in the middle. So the app deliberately skips its own ESRI publish to avoid sending everything twice:

// When Event Hub ingestion is active, ESRI Velocity has its own consumer group
// on the same event hub and receives detections directly, so we skip publishing here.
if (_condorEventSourceOptions.UseEventHub) continue;
Gotcha: the ESRI publish is mode-dependent, not unconditional

It's tempting to read ProcessCameraEvent and conclude "detections always go to ESRI." They don't — the Velocity publish is gated on !UseEventHub. If you're debugging missing ESRI updates, the first question is which mode is active: in event-hub mode, this app is supposed to be silent toward ESRI, and the fix (if any) is on the hub/consumer-group side, not here.

A separate path in: detection videos​

Detections aren't only images. Condor also produces short video clips, and those arrive by a completely different route — one that ignores the UseEventHub switch entirely. When Condor writes a clip to its storage account, a blob trigger fires:

[Function(nameof(ProcessDetectionVideo))]
public async Task ProcessDetectionVideo(
[BlobTrigger("output/detections/{name}.mp4", Connection = "CondorStorageConnectionString")] BlobClient video,
string name) { … }

It's bound to a BlobClient, not a stream, so the mp4 body is never pulled across to the worker — only the blob name is needed, because the name carries everything: Condor names clips <deviceId>_<yyyyMMddHHmmss>_video. From that, ProcessDetectionVideo:

  1. Parses the device id and timestamp from the name, resolves the camera, and applies the same environment gate the image path does — Condor writes to one storage account for every environment, so this trigger fires everywhere and must step out unless the camera belongs to this env.
  2. Matches the clip to its image detection — the nearest non-video CondorCameraEvent within a ±2-minute window — so the video inherits that detection's position, category, and fire scores. A clip with no match still produces a video event, just without image metadata.
  3. Writes a video CondorCameraEvent (isVideoDetection = true) whose OutputImageUrl is the clip, keeps the camera's active Condor event alive (refreshing its 3-hour TTL), sets the HasRecentVideoDetectionHit operational flag, and broadcasts the event to the frontend over SignalR — the same channel image detections use.
Gotcha: video rows are deduped on the clip URL, not the Condor event id

A video event's CondorCameraEventId is deliberately left null. Reusing the matched image event's id would collide with the unique-id constraint and overwrite that image row on upsert; instead, video rows are deduped on their unique clip URL. So an image detection and its video are two distinct rows referencing the same incident, not one row updated twice.

Scores on the hub path​

Both sources carry Condor's event maxima, max_fire_score / max_smoke_score (the highest box score per category, computed by Condor). The REST payload always had them; the hub message gained them in October 2026 (Alert.CA.CONDOR build_event_message). MapToCameraEventDTO passes them through and never recomputes them: Condor owns that definition. A message without them, from an older producer, maps to NULL scores, the same as a REST event with no boxes. Before the producer change the hub path always wrote NULL, the confidence-threshold procedures coalesced that to 0, and no camera cleared the AI-detected filter in any environment running on the hub (uat). confidenceScore on the message is not a substitute: it lives in the optional localization block and is the confidence of the one geolocated box.

Wrapping up​

The Notification app ingests Condor detections along three routes. Image detections come through two mutually-exclusive lanes — a 30-second poll (current) and an event-hub push (the target), selected by UseEventHub and both feeding one internal ProcessCameraEvent; the decisive difference between them is ESRI (the poll path publishes to ESRI Velocity, the event-hub path stays internal because ESRI reads the hub directly). Detection videos come through a third, always-on blob trigger that matches each clip to its image detection and records a video event. All three end the same way: internal state updated and the frontend notified over SignalR.

However a detection gets in, they all converge on one reconcile. Next, what that reconcile does with it — and every store it touches: From Detection to Event.