Parallel expert review of the four-commit scaffold surfaced 13 actionable
items split across API design, ONVIF domain accuracy, Go idiomaticity and
test rigor. This change addresses them before the Stream type lands, when
the public surface is still cheap to move.
API shape (hard-to-reverse before tagging)
------------------------------------------
* Rename EventKind -> Kind and EventState -> State to avoid the
stream.EventKind / stream.EventState stutter when imported.
* Restructure Event for non-lossy decode:
- Source string and RawValue string replaced with Source/Data maps so
multi-item ONVIF Source and Data lists (e.g. AXIS AOA emitting
active+classType+confidence; DigitalInput carrying InputToken+
LogicalState) are preserved.
- Add DeviceID so a single channel can fan in events from multiple
cameras.
- Add DeviceTime parsed from wsnt:UtcTime alongside the local
observation Timestamp. The earlier doc-comment decision to bake-in
'drop UtcTime' was a policy disguised as an API; expose both and let
callers choose.
Classifier accuracy (ONVIF domain audit)
----------------------------------------
* Introduce KindImageQuality for tns1:VideoSource/ImageTooDark|Bright|
Blurry. These are imaging-quality alarms that integrators route
separately because they fire on sunset/dawn/condensation, not tamper.
Previously mis-classified as KindTampering.
* Add tns1:VideoSource/GlobalSceneChange -> KindTampering, which is the
real lens-cover signal on firmwares without TamperDetector.
* Anchor the TamperDetector rule to 'TamperDetector/Tamper' so a
hypothetical 'TamperDetectorLog' path cannot match.
* Narrow MyRuleDetector from container-match to an explicit whitelist
(HumanDetect, VehicleDetect, PeopleDetect, ObjectsInside, FaceDetect).
Bosch publishes Counter and Occupancy under MyRuleDetector too; those
must not classify as ObjectDetected.
* Add the AXIS Guard suite (MotionGuard, FenceGuard, LoiteringGuard) ->
KindMotion. Common on AXIS deployments configured with these apps
instead of basic VMD.
* Drop the bogus Device1ScenarioANY test fixture; AOA uses numeric
scenarios (Device1Scenario1, Device1Scenario2). The 'ANY' suffix was a
borrow from the older Guard suite's Camera1ProfileANY pattern.
* Document the edge-trigger semantics of LineDetector/Crossed in the rule
comment so decoder consumers do not expect a State boolean.
Tests
-----
* String tests now use t.Run subtests so failures name the case.
* TestKindStringsAreUnique guards against accidental String() aliasing
when adding new kinds.
* TestEventFieldAssignmentRoundTrip exercises the new field set
including DeviceID, Source/Data maps and DeviceTime.
* Canonicalisation table now covers: double slash, colon-only segment,
trailing colon, multi-colon-in-segment, leading/trailing slash,
no-colon passthrough. Locks the actual behaviour so future refactors
see regressions.
* False-positive negatives: Counter and Occupancy under MyRuleDetector,
AudioEncoderConfiguration, RelayFailure, DigitalInputConfiguration,
TamperDetectorLog, MotionRecording/Started — all assert KindUnknown.
* TestClassifyRuleOrder_ObjectAnalyticsBeforeGenericObjects pins the
ordering invariant called out by the architect reviewer.
Documentation
-------------
* doc.go trimmed so it does not advertise NewStream / Events / Errors /
Close before those identifiers exist — the godoc reader will no longer
see dead names. Re-expanded when the Stream type lands.
Deferred to the Stream commit
-----------------------------
* PropertyUnknown vs PropertyUnset disambiguation — kept as
PropertyUnknown for now with a clarified doc comment; revisit when the
decoder needs to distinguish 'absent on wire' from 'unparseable'.
* Classifier pluggability (WithClassifier option) — meaningful only once
there is a Stream; revisit at that commit.
The rest of github.com/kerberos-io/onvif uses stretchr/testify (assert,
require) consistently — Device_test.go, event/type_test.go,
media2/types_test.go, ws-discovery/networking_test.go. Migrate the two
new test files in event/stream from stdlib t.Errorf to the same testify
convention so the package fits in without local style variation.
No production-code change; no behaviour change.
Cross-checked each topic string against public sources before adding the
rule, and inlined the citation next to the rule it supports so future
maintainers can audit the table:
* Hikvision motion: CellMotionDetector/Motion (Hikvision PDF on third
party motion troubleshooting) plus the VideoSource/MotionAlarm
fallback emitted by newer firmware.
* Hikvision tamper-class scene change: VideoSource/ImageTooDark|Bright|
Blurry — present in the ONVIF topic namespace; treated as Tampering
for routing.
* Bosch motion: VideoAnalytics/MotionAlarm (Bosch metadata/IVA PDF) —
NOT VideoSource/MotionAlarm. The earlier 'tnsbosch:MotionAlarm' guess
in PR #194 was wrong; Bosch uses standard tns1 namespace under
VideoAnalytics.
* Hanwha (Samsung Wisenet): VideoAnalytics/tnssamsung:MotionDetection,
VideoAnalytics/tnssamsung:TamperingDetection,
AudioAnalytics/tnssamsung:SoundDetection — confirmed via HA #66493
capture.
* Avigilon: per-segment-namespaced serialisation
(tns1:Device/tns1:Trigger/tns1:Relay) folded by canonicalization.
Documented in Avigilon's own ONVIF subscription guide.
* Object analytics: LineDetector/Crossed, FieldDetector/ObjectsInside
and the MyRuleDetector container for vendor rule names (Bosch IVA,
Dahua SMD) — sourced from ONVIF Analytics Service Spec v22.06.
* AXIS Object Analytics: prefix match on ObjectAnalytics/ to absorb
the dynamic Device1Scenario<N> suffixes (AXIS counting-data docs).
Empirical topic table cross-checked with openvideolibs/onvif-parsers
(Apache-2.0), the package the Home Assistant ONVIF integration imports —
referenced from the package doc-comment.
Test cases now cover the verified topic for every supported vendor plus
case-sensitivity and canonicalization. No code change for callers: the
public API is still just Classify(topic) -> 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.
Introduces a new event/stream sub-package that will host the long-running,
channel-based consumer for ONVIF device events. This commit only lays down
the value types — EventKind, EventState, PropertyOperation and the Event
struct — together with Stringer methods and zero-value tests.
The intent is to give callers a vendor-neutral surface (Motion, DigitalInput,
etc.) so they do not need to special-case AXIS, Hikvision, Avigilon, Hanwha,
Bosch or Dahua topic strings. Decoding, topic classification and the Stream
type itself land in follow-up commits.