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:
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;
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:
- 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.
- Matches the clip to its image detection — the nearest non-video
CondorCameraEventwithin 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. - Writes a video
CondorCameraEvent(isVideoDetection = true) whoseOutputImageUrlis the clip, keeps the camera's active Condor event alive (refreshing its 3-hour TTL), sets theHasRecentVideoDetectionHitoperational flag, and broadcasts the event to the frontend over SignalR — the same channel image detections use.
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.