Files
onvif/event/stream/topics_test.go
Sebastian Norling 2cc266714e feat(event/stream): classify vendor topics to normalized EventKind
Adds a Classify function that maps ONVIF topic strings to EventKind so the
agent does not need to know AXIS vs Hikvision vs Bosch topic conventions.

The classifier canonicalizes topics by stripping XML-namespace prefixes from
each path segment, which collapses vendor variants like
'tns1:Device/tnssamsung:DigitalInput' and 'tns1:Device/Trigger/DigitalInput'
to a single matchable form.

Motion coverage on day one:
  * tns1:VideoSource/MotionAlarm  (AXIS, Bosch, Dahua, ...)
  * tns1:VideoAnalytics/<vendor>:MotionAlarm
  * tns1:RuleEngine/CellMotionDetector/Motion  (ONVIF standard, Hikvision)
  * tns1:RuleEngine/MotionRegionDetector/Motion  (AXIS region rule)
  * tnsaxis:CameraApplicationPlatform/ObjectAnalytics/...

Also covers Tamper, DigitalInput, Relay (DigitalOutput), object analytics
and audio alarms, so the same Stream can replace the agent's ad-hoc digital
I/O polling without losing coverage.
2026-05-21 13:59:22 +02:00

54 lines
2.4 KiB
Go

package stream
import "testing"
func TestClassifyTopic(t *testing.T) {
tests := []struct {
name string
topic string
want EventKind
}{
// AXIS: motion alarm on video source.
{"axis_video_source_motion", "tns1:VideoSource/MotionAlarm", KindMotion},
// AXIS / Hikvision / others: ONVIF cell-motion detector rule.
{"cell_motion_detector", "tns1:RuleEngine/CellMotionDetector/Motion", KindMotion},
// AXIS region motion.
{"motion_region_detector", "tns1:RuleEngine/MotionRegionDetector/Motion", KindMotion},
// Vendor-prefixed motion (e.g. Bosch).
{"bosch_motion", "tns1:VideoAnalytics/tnsbosch:MotionAlarm", KindMotion},
// Tampering / scene change.
{"tamper_detector", "tns1:RuleEngine/TamperDetector/Tamper", KindTampering},
{"axis_scene_tamper", "tns1:VideoSource/ImageTooDark/ImagingService", KindUnknown}, // not classified
// Digital input — vendor-namespaced variant from agent code.
{"digital_input", "tns1:Device/Trigger/DigitalInput", KindDigitalInput},
{"digital_input_samsung", "tns1:Device/tns1:Trigger/tnssamsung:DigitalInput", KindDigitalInput},
// Digital output / relay.
{"relay", "tns1:Device/Trigger/Relay", KindDigitalOutput},
{"relay_avigilon", "tns1:Device/tns1:Trigger/tns1:Relay", KindDigitalOutput},
// Analytics object detection (AXIS object analytics, generic motion analytics).
{"object_detected", "tns1:RuleEngine/MyRuleDetector/ObjectsInside", KindObjectDetected},
{"axis_object_analytics", "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1ScenarioANY", KindObjectDetected},
// Audio.
{"audio_alarm", "tns1:AudioAnalytics/Audio/DetectedSound", KindAudioAlarm},
{"axis_audio", "tnsaxis:AudioSource/TriggerLevel", KindAudioAlarm},
// Unknown — should not be force-classified.
{"empty", "", KindUnknown},
{"unknown_topic", "tns1:UserAlarm/IVA", KindUnknown},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := Classify(tc.topic); got != tc.want {
t.Errorf("Classify(%q) = %v, want %v", tc.topic, got, tc.want)
}
})
}
}
func TestClassifyIsCaseSensitive(t *testing.T) {
// ONVIF topic names are case-sensitive per spec; we should not silently
// upper/lower-case. A lowercased topic must not match.
if got := Classify("tns1:videosource/motionalarm"); got != KindUnknown {
t.Errorf("Classify lowercase = %v, want KindUnknown (topics are case-sensitive)", got)
}
}