mirror of
https://github.com/kerberos-io/onvif.git
synced 2026-08-23 15:08:33 +00:00
feat(event/stream): scaffold package with normalized event types
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.
This commit is contained in:
13
event/stream/doc.go
Normal file
13
event/stream/doc.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// Package stream provides a long-running, channel-based consumer for ONVIF
|
||||
// device events. It hides the SOAP/XML, pull-point lifecycle, renewal and
|
||||
// vendor-specific topic conventions behind a typed Event stream.
|
||||
//
|
||||
// A Stream is created with NewStream and yields decoded Event values on the
|
||||
// channel returned by Events. Non-fatal errors (transient SOAP failures that
|
||||
// the stream recovers from) are surfaced on Errors. The Stream is stopped by
|
||||
// cancelling the context passed to NewStream or by calling Close.
|
||||
//
|
||||
// The package classifies vendor-specific topic strings (AXIS, Hikvision,
|
||||
// Avigilon, Hanwha, Bosch, Dahua) into a small set of normalized EventKind
|
||||
// values so callers do not need to special-case device manufacturers.
|
||||
package stream
|
||||
130
event/stream/types.go
Normal file
130
event/stream/types.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EventKind is the normalized category of an ONVIF event, independent of the
|
||||
// camera vendor's topic naming.
|
||||
type EventKind uint8
|
||||
|
||||
const (
|
||||
// KindUnknown is the zero value; used when a topic does not match any
|
||||
// known classification.
|
||||
KindUnknown EventKind = iota
|
||||
// KindMotion covers motion detection from any vendor (e.g. AXIS
|
||||
// VideoSource/MotionAlarm, Hikvision RuleEngine/CellMotionDetector).
|
||||
KindMotion
|
||||
// KindTampering covers camera tampering / scene change alarms.
|
||||
KindTampering
|
||||
// KindDigitalInput covers external sensor inputs wired to the camera.
|
||||
KindDigitalInput
|
||||
// KindDigitalOutput covers relay output state changes on the camera.
|
||||
KindDigitalOutput
|
||||
// KindObjectDetected covers analytics-based object/person/vehicle
|
||||
// detection events.
|
||||
KindObjectDetected
|
||||
// KindAudioAlarm covers audio-level / loud-noise alarms.
|
||||
KindAudioAlarm
|
||||
)
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
func (k EventKind) String() string {
|
||||
switch k {
|
||||
case KindUnknown:
|
||||
return "Unknown"
|
||||
case KindMotion:
|
||||
return "Motion"
|
||||
case KindTampering:
|
||||
return "Tampering"
|
||||
case KindDigitalInput:
|
||||
return "DigitalInput"
|
||||
case KindDigitalOutput:
|
||||
return "DigitalOutput"
|
||||
case KindObjectDetected:
|
||||
return "ObjectDetected"
|
||||
case KindAudioAlarm:
|
||||
return "AudioAlarm"
|
||||
default:
|
||||
return fmt.Sprintf("EventKind(%d)", uint8(k))
|
||||
}
|
||||
}
|
||||
|
||||
// EventState is the active/inactive state carried by an event. Most ONVIF
|
||||
// alarms are boolean (e.g. IsMotion=true/false); StateUnknown is used when
|
||||
// the value cannot be parsed.
|
||||
type EventState uint8
|
||||
|
||||
const (
|
||||
StateUnknown EventState = iota
|
||||
StateActive
|
||||
StateInactive
|
||||
)
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
func (s EventState) String() string {
|
||||
switch s {
|
||||
case StateUnknown:
|
||||
return "Unknown"
|
||||
case StateActive:
|
||||
return "Active"
|
||||
case StateInactive:
|
||||
return "Inactive"
|
||||
default:
|
||||
return fmt.Sprintf("EventState(%d)", uint8(s))
|
||||
}
|
||||
}
|
||||
|
||||
// PropertyOperation mirrors the ONVIF wsnt:PropertyOperation attribute and
|
||||
// indicates whether a message is the first sighting of a property
|
||||
// (Initialized), a transition (Changed) or the property going away (Deleted).
|
||||
type PropertyOperation uint8
|
||||
|
||||
const (
|
||||
PropertyUnknown PropertyOperation = iota
|
||||
PropertyInitialized
|
||||
PropertyChanged
|
||||
PropertyDeleted
|
||||
)
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
func (p PropertyOperation) String() string {
|
||||
switch p {
|
||||
case PropertyUnknown:
|
||||
return "Unknown"
|
||||
case PropertyInitialized:
|
||||
return "Initialized"
|
||||
case PropertyChanged:
|
||||
return "Changed"
|
||||
case PropertyDeleted:
|
||||
return "Deleted"
|
||||
default:
|
||||
return fmt.Sprintf("PropertyOperation(%d)", uint8(p))
|
||||
}
|
||||
}
|
||||
|
||||
// Event is a single normalized notification from an ONVIF device.
|
||||
//
|
||||
// Kind, State and Operation are the normalized fields most callers should
|
||||
// switch on. Topic, RawValue and Source preserve the original ONVIF data so
|
||||
// callers can do further inspection or logging without re-parsing SOAP.
|
||||
type Event struct {
|
||||
// Kind is the normalized event category.
|
||||
Kind EventKind
|
||||
// State is the active/inactive value carried by the event.
|
||||
State EventState
|
||||
// Operation is the ONVIF property lifecycle (Initialized/Changed/Deleted).
|
||||
Operation PropertyOperation
|
||||
// Source identifies the channel, input, or rule that produced the event
|
||||
// (taken from the Source SimpleItem in the notification).
|
||||
Source string
|
||||
// Topic is the raw ONVIF topic string, e.g. tns1:VideoSource/MotionAlarm.
|
||||
Topic string
|
||||
// RawValue is the unparsed Data SimpleItem value (e.g. "true", "1",
|
||||
// "active") so callers can read non-boolean values when needed.
|
||||
RawValue string
|
||||
// Timestamp is when the stream observed the event locally. The ONVIF
|
||||
// UtcTime is not used because clocks on many cameras drift.
|
||||
Timestamp time.Time
|
||||
}
|
||||
83
event/stream/types_test.go
Normal file
83
event/stream/types_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestEventKindString(t *testing.T) {
|
||||
tests := []struct {
|
||||
kind EventKind
|
||||
want string
|
||||
}{
|
||||
{KindUnknown, "Unknown"},
|
||||
{KindMotion, "Motion"},
|
||||
{KindTampering, "Tampering"},
|
||||
{KindDigitalInput, "DigitalInput"},
|
||||
{KindDigitalOutput, "DigitalOutput"},
|
||||
{KindObjectDetected, "ObjectDetected"},
|
||||
{KindAudioAlarm, "AudioAlarm"},
|
||||
{EventKind(255), "EventKind(255)"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := tc.kind.String(); got != tc.want {
|
||||
t.Errorf("EventKind(%d).String() = %q, want %q", tc.kind, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventStateString(t *testing.T) {
|
||||
tests := []struct {
|
||||
state EventState
|
||||
want string
|
||||
}{
|
||||
{StateUnknown, "Unknown"},
|
||||
{StateActive, "Active"},
|
||||
{StateInactive, "Inactive"},
|
||||
{EventState(255), "EventState(255)"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := tc.state.String(); got != tc.want {
|
||||
t.Errorf("EventState(%d).String() = %q, want %q", tc.state, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPropertyOperationString(t *testing.T) {
|
||||
tests := []struct {
|
||||
op PropertyOperation
|
||||
want string
|
||||
}{
|
||||
{PropertyUnknown, "Unknown"},
|
||||
{PropertyInitialized, "Initialized"},
|
||||
{PropertyChanged, "Changed"},
|
||||
{PropertyDeleted, "Deleted"},
|
||||
{PropertyOperation(255), "PropertyOperation(255)"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := tc.op.String(); got != tc.want {
|
||||
t.Errorf("PropertyOperation(%d).String() = %q, want %q", tc.op, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventZeroValue(t *testing.T) {
|
||||
var e Event
|
||||
if e.Kind != KindUnknown {
|
||||
t.Errorf("zero Event.Kind = %v, want KindUnknown", e.Kind)
|
||||
}
|
||||
if e.State != StateUnknown {
|
||||
t.Errorf("zero Event.State = %v, want StateUnknown", e.State)
|
||||
}
|
||||
if !e.Timestamp.IsZero() {
|
||||
t.Errorf("zero Event.Timestamp = %v, want zero time", e.Timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventTimestampPreserved(t *testing.T) {
|
||||
now := time.Now()
|
||||
e := Event{Timestamp: now}
|
||||
if !e.Timestamp.Equal(now) {
|
||||
t.Errorf("Event.Timestamp = %v, want %v", e.Timestamp, now)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user