Skip to content

Universal ONVIF event taxonomy

NovaVMS sees a lot of different cameras. Tapo, Hikvision, Dahua, Vivotek, Axis, and Bosch all speak ONVIF, but each one labels its events differently — one vendor’s “line crossing” is another vendor’s “rule engine intrusion detector,” and a third vendor invents a brand-new topic for a feature nobody else ships. This page explains how NovaVMS keeps every one of those events visible to you without forcing us to ship a code change every time a new camera arrives.

Why a universal taxonomy?

Early versions of NovaVMS used a fixed list of trigger types: motion, software motion, manual, and scheduled. That list was a hard wall. When a camera fired anything else — a tamper alarm, an intrusion line crossing, an audio detection — the cloud silently dropped it because the value didn’t match the enum. An audit in April 2026 found that four of the five event categories the gateway was already detecting never reached the database. Operators were authoring alert rules against events that, from the cloud’s perspective, did not exist.

The redesign (per D95) had two goals: zero events lost, and zero per-camera filter for us to maintain as new cameras ship. We did this by splitting the trigger into two columns. A coarse event_category enum gives you a stable dimension to filter and author alert rules against. A separate trigger_topic text field preserves the original ONVIF topic verbatim — so the granular detail is always there even when the category bucket is broad.

The 10 categories

Every event lands in exactly one of these buckets. The category names match the database enum, but in plain English they mean:

  • motion — generic motion detected by the camera or a scene-change detector. Examples: tns1:VideoSource/MotionAlarm, tns1:RuleEngine/MotionRegionDetector/Motion.
  • intrusion — somebody crossed into a region you defined, lingered too long, or breached a virtual fence. Examples: tns1:RuleEngine/FieldDetector/ObjectsInside, tns1:RuleEngine/LoiteringDetector/Loitering.
  • line_crossing — a directional line was crossed. Some vendors model this as a sub-class of intrusion; we keep it separate because it’s the most common rule you’ll author. Example: tns1:RuleEngine/LineDetector/Crossed.
  • tamper — somebody physically interfered with the camera. Cover, paint, blur, or focus change. Example: tns1:RuleEngine/TamperDetector/Tamper.
  • tampering_scene — the scene itself changed in a way that looks like tampering, but the camera isn’t certain (significant scene change, sudden blackout, sudden overexposure). Profile T §7.17 covers this. Example: tns1:RuleEngine/CellMotionDetector/Motion when paired with a scene-change rule.
  • analytics_object — the camera or our AI pipeline classified an object: person, vehicle, animal, or package. Example: tns1:RuleEngine/MyRuleDetector/People.
  • io — a wired digital input fired. Door switch, alarm contact, GPIO trigger, panic button. Example: tns1:Device/Trigger/DigitalInput.
  • audio — the camera detected sound above a threshold or classified it (gunshot, glass break, scream, raised voices). Example: tns1:AudioAnalytics/Audio/DetectedSound.
  • schedule — a scheduled or recurring event fired (less common; mostly used for test pulses).
  • other — every advertised topic that doesn’t fit the above. Never dropped. The full topic string is kept in trigger_topic so you can still author alerts against it.

The trigger_topic field — verbatim preservation

Every event row keeps the original normalized ONVIF topic in a trigger_topic column, regardless of which category it landed in. This means an alert rule can target the raw topic via a trigger_topic_pattern SQL LIKE pattern — for example %LineDetector% to match every directional crossing topic across vendors, or %TamperDetector% to catch tamper alerts whether the vendor labels them under tamper or tampering_scene. See the alert rule schema for the full pattern syntax.

Where the classification happens

The mapping from ONVIF topic to event_category lives on the gateway, not the cloud (per D96). It’s a static Go slice called topicRegistry in gateway/internal/onvif/event_state.go. Every advertised topic is checked against the registry; matches return their category, and unmatched topics return other. We keep the registry on the gateway for two reasons: it’s the natural source of the event (no cross-layer trip to ask “what category is this?”) and adding support for a new vendor topic is one slice entry plus a gateway redeploy — no migration, no cloud code change, no frontend change.

Cameras with limited ONVIF — the degradation ladder

The ONVIF spec is enormous and few cameras implement all of it. NovaVMS treats it as a menu of best-effort operations rather than a hard contract (per D97). The minimum-viable op set is just three calls: CreatePullPointSubscription to start receiving events, PullMessages to fetch them, and Unsubscribe to clean up. Every other op — filtered subscribes, capability discovery, synchronization points, renew — is optional and non-blocking. If the camera doesn’t support it, we log a warning and continue. This is why an old Tapo or a barebones generic ONVIF camera still gives you events even when its spec compliance is patchy.

The gateway also subscribes to every advertised topic with no server-side filter (per D98). Topic classification happens after delivery. This is the rule that turned the four silently-dropped categories into four visible categories. If a camera fires a brand-new topic we’ve never seen, it shows up as other with the raw topic preserved — and you can immediately author an alert rule against it.

Per-topic clip cooldown

Subscribing to everything has a cost. Some cameras are chatty — a Hikvision in a busy parking lot can emit dozens of Monitoring/ProcessorUsage events per minute, and a Vivotek can re-fire Tampering every second when it disagrees with itself. To prevent these storms from saturating clip uploads, the gateway enforces a minimum interval between successful clip recordings for the same (camera_id, normalized_topic) pair (per D99). The default is 30 seconds, tunable via GATEWAY_EVENT_CLIP_COOLDOWN_SEC.

Suppressed firings never become events (per D114). During a storm you see clean, evenly spaced events — one captured clip per cooldown window — rather than a wall of clip-less rows. The firings inside the window are dropped at the gateway before anything is sent to the cloud, so nothing about them appears in the events list. If you ever need to confirm a storm is being throttled, the gateway’s log records one line per suppressed firing.

What this means for alert authoring

Two rule kinds map onto this model:

  • rule_kind: camera_event — match by event_category (or leave blank to match any category) plus optional trigger_topic_pattern for granular matching plus optional source_filter to restrict to ONVIF or manual triggers.
  • rule_kind: system_event — match by system_trigger for camera/gateway lifecycle events (camera_offline, gateway_offline, gateway_reconnect).

For the full schema, see alert rule schema. For the wizard walk-through, see Create an alert rule.

See also

For the design history, see decisions D95 through D104 in the internal decisions log.