Merge pull request #47 from 0x524a/46-feature-add-more-event-operations
feat: implement Event and Device IO services with CLI integration
This commit is contained in:
@@ -99,6 +99,16 @@ linters:
|
||||
linters:
|
||||
- dupl
|
||||
|
||||
- path: deviceio\.go
|
||||
linters:
|
||||
- dupl
|
||||
|
||||
- path: event\.go
|
||||
linters:
|
||||
- dupl
|
||||
- gocritic
|
||||
- staticcheck
|
||||
|
||||
- path: examples/
|
||||
linters:
|
||||
- errcheck
|
||||
|
||||
+589
-1
@@ -58,6 +58,10 @@ func main() {
|
||||
cli.ptzOperations()
|
||||
case "6":
|
||||
cli.imagingOperations()
|
||||
case "7":
|
||||
cli.eventOperations()
|
||||
case "8":
|
||||
cli.deviceIOOperations()
|
||||
case "0", "q", "quit", "exit":
|
||||
fmt.Println("Goodbye! 👋")
|
||||
|
||||
@@ -78,8 +82,10 @@ func (c *CLI) showMainMenu() {
|
||||
fmt.Println(" 4. Media Operations")
|
||||
fmt.Println(" 5. PTZ Operations")
|
||||
fmt.Println(" 6. Imaging Operations")
|
||||
fmt.Println(" 7. Event Operations")
|
||||
fmt.Println(" 8. Device IO Operations")
|
||||
} else {
|
||||
fmt.Println(" 3-6. (Connect to camera first)")
|
||||
fmt.Println(" 3-8. (Connect to camera first)")
|
||||
}
|
||||
fmt.Println(" 0. Exit")
|
||||
fmt.Println()
|
||||
@@ -1625,3 +1631,585 @@ func (c *CLI) captureAndDisplaySnapshot(ctx context.Context) { //nolint:funlen /
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Event Operations
|
||||
// ============================================
|
||||
|
||||
func (c *CLI) eventOperations() {
|
||||
if c.client == nil {
|
||||
fmt.Println("❌ Not connected to any camera")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("📡 Event Operations")
|
||||
fmt.Println("==================")
|
||||
fmt.Println(" 1. Get Event Service Capabilities")
|
||||
fmt.Println(" 2. Get Event Properties")
|
||||
fmt.Println(" 3. Create Pull Point Subscription")
|
||||
fmt.Println(" 4. Get Event Brokers")
|
||||
fmt.Println(" 0. Back to Main Menu")
|
||||
|
||||
choice := c.readInput("Select operation: ")
|
||||
ctx := context.Background()
|
||||
|
||||
switch choice {
|
||||
case "1":
|
||||
c.getEventServiceCapabilities(ctx)
|
||||
case "2":
|
||||
c.getEventProperties(ctx)
|
||||
case "3":
|
||||
c.createPullPointSubscription(ctx)
|
||||
case "4":
|
||||
c.getEventBrokers(ctx)
|
||||
case "0":
|
||||
return
|
||||
default:
|
||||
fmt.Println("❌ Invalid option")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CLI) getEventServiceCapabilities(ctx context.Context) {
|
||||
fmt.Println("⏳ Getting event service capabilities...")
|
||||
|
||||
caps, err := c.client.GetEventServiceCapabilities(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✅ Event Service Capabilities:")
|
||||
fmt.Printf(" WS Subscription Policy Support: %v\n", caps.WSSubscriptionPolicySupport)
|
||||
fmt.Printf(" WS Pausable Subscription: %v\n", caps.WSPausableSubscriptionManagerInterfaceSupport)
|
||||
fmt.Printf(" Max Notification Producers: %d\n", caps.MaxNotificationProducers)
|
||||
fmt.Printf(" Max Pull Points: %d\n", caps.MaxPullPoints)
|
||||
fmt.Printf(" Persistent Notification Storage: %v\n", caps.PersistentNotificationStorage)
|
||||
fmt.Printf(" Event Broker Protocols: %v\n", caps.EventBrokerProtocols)
|
||||
fmt.Printf(" Max Event Brokers: %d\n", caps.MaxEventBrokers)
|
||||
fmt.Printf(" Metadata Over MQTT: %v\n", caps.MetadataOverMQTT)
|
||||
}
|
||||
|
||||
func (c *CLI) getEventProperties(ctx context.Context) {
|
||||
fmt.Println("⏳ Getting event properties...")
|
||||
|
||||
props, err := c.client.GetEventProperties(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✅ Event Properties:")
|
||||
fmt.Printf(" Fixed Topic Set: %v\n", props.FixedTopicSet)
|
||||
fmt.Printf(" Topic Namespace Locations: %d\n", len(props.TopicNamespaceLocation))
|
||||
for i, loc := range props.TopicNamespaceLocation {
|
||||
fmt.Printf(" %d. %s\n", i+1, loc)
|
||||
}
|
||||
fmt.Printf(" Topic Expression Dialects: %d\n", len(props.TopicExpressionDialects))
|
||||
fmt.Printf(" Message Content Filter Dialects: %d\n", len(props.MessageContentFilterDialects))
|
||||
}
|
||||
|
||||
func (c *CLI) createPullPointSubscription(ctx context.Context) {
|
||||
fmt.Println("⏳ Creating pull point subscription...")
|
||||
|
||||
termTimeStr := c.readInputWithDefault("Subscription duration (seconds)", "60")
|
||||
termTimeSec, err := strconv.Atoi(termTimeStr)
|
||||
if err != nil || termTimeSec <= 0 {
|
||||
termTimeSec = 60
|
||||
}
|
||||
|
||||
termTime := time.Duration(termTimeSec) * time.Second
|
||||
|
||||
sub, err := c.client.CreatePullPointSubscription(ctx, "", &termTime, "")
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✅ Pull Point Subscription Created:")
|
||||
fmt.Printf(" Subscription Reference: %s\n", sub.SubscriptionReference)
|
||||
fmt.Printf(" Current Time: %v\n", sub.CurrentTime)
|
||||
fmt.Printf(" Termination Time: %v\n", sub.TerminationTime)
|
||||
|
||||
// Offer to pull messages
|
||||
pull := c.readInput("📨 Pull messages now? (y/n) [y]: ")
|
||||
if pull == "" || strings.EqualFold(pull, "y") {
|
||||
c.pullMessagesFromSubscription(ctx, sub.SubscriptionReference)
|
||||
}
|
||||
|
||||
// Offer to unsubscribe
|
||||
unsub := c.readInput("🔌 Unsubscribe? (y/n) [y]: ")
|
||||
if unsub == "" || strings.EqualFold(unsub, "y") {
|
||||
if err := c.client.Unsubscribe(ctx, sub.SubscriptionReference); err != nil {
|
||||
fmt.Printf("❌ Unsubscribe error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("✅ Unsubscribed successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CLI) pullMessagesFromSubscription(ctx context.Context, subscriptionRef string) {
|
||||
fmt.Println("⏳ Pulling messages (5 second timeout)...")
|
||||
|
||||
messages, err := c.client.PullMessages(ctx, subscriptionRef, 5*time.Second, 100) //nolint:mnd // 100 max messages
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(messages) == 0 {
|
||||
fmt.Println("📭 No messages available")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Received %d message(s):\n", len(messages))
|
||||
for i := range messages {
|
||||
msg := &messages[i]
|
||||
if i >= 10 { //nolint:mnd // Show max 10 messages
|
||||
fmt.Printf(" ... and %d more\n", len(messages)-10) //nolint:mnd // Show remaining count
|
||||
|
||||
break
|
||||
}
|
||||
fmt.Printf(" %d. Topic: %s\n", i+1, msg.Topic)
|
||||
if msg.Message.PropertyOperation != "" {
|
||||
fmt.Printf(" Operation: %s\n", msg.Message.PropertyOperation)
|
||||
}
|
||||
if !msg.Message.UtcTime.IsZero() {
|
||||
fmt.Printf(" Time: %v\n", msg.Message.UtcTime)
|
||||
}
|
||||
if len(msg.Message.Source) > 0 {
|
||||
fmt.Printf(" Source: %s=%s\n", msg.Message.Source[0].Name, msg.Message.Source[0].Value)
|
||||
}
|
||||
if len(msg.Message.Data) > 0 {
|
||||
fmt.Printf(" Data: %s=%s\n", msg.Message.Data[0].Name, msg.Message.Data[0].Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CLI) getEventBrokers(ctx context.Context) {
|
||||
fmt.Println("⏳ Getting event brokers...")
|
||||
|
||||
brokers, err := c.client.GetEventBrokers(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(brokers) == 0 {
|
||||
fmt.Println("📭 No event brokers configured")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Found %d Event Broker(s):\n", len(brokers))
|
||||
for i, broker := range brokers {
|
||||
fmt.Printf(" %d. Address: %s\n", i+1, broker.Address)
|
||||
if broker.TopicPrefix != "" {
|
||||
fmt.Printf(" Topic Prefix: %s\n", broker.TopicPrefix)
|
||||
}
|
||||
if broker.Status != "" {
|
||||
fmt.Printf(" Status: %s\n", broker.Status)
|
||||
}
|
||||
fmt.Printf(" QoS: %d\n", broker.QoS)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Device IO Operations
|
||||
// ============================================
|
||||
|
||||
func (c *CLI) deviceIOOperations() {
|
||||
if c.client == nil {
|
||||
fmt.Println("❌ Not connected to any camera")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("🔌 Device IO Operations")
|
||||
fmt.Println("======================")
|
||||
fmt.Println(" 1. Get Device IO Capabilities")
|
||||
fmt.Println(" 2. Get Digital Inputs")
|
||||
fmt.Println(" 3. Get Relay Outputs")
|
||||
fmt.Println(" 4. Set Relay Output State")
|
||||
fmt.Println(" 5. Get Relay Output Options")
|
||||
fmt.Println(" 6. Get Video Outputs")
|
||||
fmt.Println(" 7. Get Video Output Configuration")
|
||||
fmt.Println(" 8. Get Video Output Configuration Options")
|
||||
fmt.Println(" 9. Get Serial Ports")
|
||||
fmt.Println(" 0. Back to Main Menu")
|
||||
|
||||
choice := c.readInput("Select operation: ")
|
||||
ctx := context.Background()
|
||||
|
||||
switch choice {
|
||||
case "1":
|
||||
c.getDeviceIOCapabilities(ctx)
|
||||
case "2":
|
||||
c.getDigitalInputs(ctx)
|
||||
case "3":
|
||||
c.getRelayOutputsCLI(ctx)
|
||||
case "4":
|
||||
c.setRelayOutputStateCLI(ctx)
|
||||
case "5":
|
||||
c.getRelayOutputOptionsCLI(ctx)
|
||||
case "6":
|
||||
c.getVideoOutputsCLI(ctx)
|
||||
case "7":
|
||||
c.getVideoOutputConfigurationCLI(ctx)
|
||||
case "8":
|
||||
c.getVideoOutputConfigurationOptionsCLI(ctx)
|
||||
case "9":
|
||||
c.getSerialPortsCLI(ctx)
|
||||
case "0":
|
||||
return
|
||||
default:
|
||||
fmt.Println("❌ Invalid option")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CLI) getDeviceIOCapabilities(ctx context.Context) {
|
||||
fmt.Println("⏳ Getting Device IO capabilities...")
|
||||
|
||||
caps, err := c.client.GetDeviceIOServiceCapabilities(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✅ Device IO Capabilities:")
|
||||
fmt.Printf(" Video Sources: %d\n", caps.VideoSources)
|
||||
fmt.Printf(" Video Outputs: %d\n", caps.VideoOutputs)
|
||||
fmt.Printf(" Audio Sources: %d\n", caps.AudioSources)
|
||||
fmt.Printf(" Audio Outputs: %d\n", caps.AudioOutputs)
|
||||
fmt.Printf(" Relay Outputs: %d\n", caps.RelayOutputs)
|
||||
fmt.Printf(" Digital Inputs: %d\n", caps.DigitalInputs)
|
||||
fmt.Printf(" Serial Ports: %d\n", caps.SerialPorts)
|
||||
fmt.Printf(" Digital Input Options: %v\n", caps.DigitalInputOptions)
|
||||
fmt.Printf(" Serial Port Configuration: %v\n", caps.SerialPortConfiguration)
|
||||
}
|
||||
|
||||
func (c *CLI) getDigitalInputs(ctx context.Context) {
|
||||
fmt.Println("⏳ Getting digital inputs...")
|
||||
|
||||
inputs, err := c.client.GetDigitalInputs(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(inputs) == 0 {
|
||||
fmt.Println("📭 No digital inputs found")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Found %d Digital Input(s):\n", len(inputs))
|
||||
for i, input := range inputs {
|
||||
fmt.Printf(" %d. Token: %s\n", i+1, input.Token)
|
||||
fmt.Printf(" Idle State: %s\n", input.IdleState)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CLI) getRelayOutputsCLI(ctx context.Context) {
|
||||
fmt.Println("⏳ Getting relay outputs...")
|
||||
|
||||
relays, err := c.client.GetRelayOutputs(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(relays) == 0 {
|
||||
fmt.Println("📭 No relay outputs found")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Found %d Relay Output(s):\n", len(relays))
|
||||
for i, relay := range relays {
|
||||
fmt.Printf(" %d. Token: %s\n", i+1, relay.Token)
|
||||
fmt.Printf(" Mode: %s\n", relay.Properties.Mode)
|
||||
fmt.Printf(" Idle State: %s\n", relay.Properties.IdleState)
|
||||
if relay.Properties.DelayTime > 0 {
|
||||
fmt.Printf(" Delay Time: %v\n", relay.Properties.DelayTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CLI) setRelayOutputStateCLI(ctx context.Context) {
|
||||
// First get available relay outputs
|
||||
relays, err := c.client.GetRelayOutputs(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error getting relays: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(relays) == 0 {
|
||||
fmt.Println("📭 No relay outputs available")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Available relay outputs:")
|
||||
for i, relay := range relays {
|
||||
fmt.Printf(" %d. %s (Mode: %s)\n", i+1, relay.Token, relay.Properties.Mode)
|
||||
}
|
||||
|
||||
choice := c.readInput("Select relay (1-" + strconv.Itoa(len(relays)) + "): ")
|
||||
idx, err := strconv.Atoi(choice)
|
||||
if err != nil || idx < 1 || idx > len(relays) {
|
||||
fmt.Println("❌ Invalid selection")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
selectedRelay := relays[idx-1]
|
||||
|
||||
fmt.Println("Select state:")
|
||||
fmt.Println(" 1. Active")
|
||||
fmt.Println(" 2. Inactive")
|
||||
stateChoice := c.readInput("State: ")
|
||||
|
||||
var state onvif.RelayLogicalState
|
||||
switch stateChoice {
|
||||
case "1":
|
||||
state = onvif.RelayLogicalStateActive
|
||||
case "2":
|
||||
state = onvif.RelayLogicalStateInactive
|
||||
default:
|
||||
fmt.Println("❌ Invalid state")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("⏳ Setting relay output state...")
|
||||
|
||||
if err := c.client.SetRelayOutputState(ctx, selectedRelay.Token, state); err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Relay %s set to %s\n", selectedRelay.Token, state)
|
||||
}
|
||||
|
||||
func (c *CLI) getVideoOutputsCLI(ctx context.Context) {
|
||||
fmt.Println("⏳ Getting video outputs...")
|
||||
|
||||
outputs, err := c.client.GetVideoOutputs(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(outputs) == 0 {
|
||||
fmt.Println("📭 No video outputs found")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Found %d Video Output(s):\n", len(outputs))
|
||||
for i, output := range outputs {
|
||||
fmt.Printf(" %d. Token: %s\n", i+1, output.Token)
|
||||
if output.Resolution != nil {
|
||||
fmt.Printf(" Resolution: %dx%d\n", output.Resolution.Width, output.Resolution.Height)
|
||||
}
|
||||
if output.RefreshRate > 0 {
|
||||
fmt.Printf(" Refresh Rate: %.1f Hz\n", output.RefreshRate)
|
||||
}
|
||||
if output.AspectRatio != "" {
|
||||
fmt.Printf(" Aspect Ratio: %s\n", output.AspectRatio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CLI) getSerialPortsCLI(ctx context.Context) {
|
||||
fmt.Println("⏳ Getting serial ports...")
|
||||
|
||||
ports, err := c.client.GetSerialPorts(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(ports) == 0 {
|
||||
fmt.Println("📭 No serial ports found")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Found %d Serial Port(s):\n", len(ports))
|
||||
for i, port := range ports {
|
||||
fmt.Printf(" %d. Token: %s\n", i+1, port.Token)
|
||||
fmt.Printf(" Type: %s\n", port.Type)
|
||||
|
||||
// Get configuration if available
|
||||
config, err := c.client.GetSerialPortConfiguration(ctx, port.Token)
|
||||
if err == nil {
|
||||
fmt.Printf(" Baud Rate: %d\n", config.BaudRate)
|
||||
fmt.Printf(" Parity: %s\n", config.ParityBit)
|
||||
fmt.Printf(" Data Bits: %d\n", config.CharacterLength)
|
||||
fmt.Printf(" Stop Bits: %.1f\n", config.StopBit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CLI) getRelayOutputOptionsCLI(ctx context.Context) {
|
||||
// First get available relay outputs
|
||||
relays, err := c.client.GetRelayOutputs(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error getting relays: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(relays) == 0 {
|
||||
fmt.Println("📭 No relay outputs available")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Available relay outputs:")
|
||||
for i, relay := range relays {
|
||||
fmt.Printf(" %d. %s\n", i+1, relay.Token)
|
||||
}
|
||||
|
||||
choice := c.readInput("Select relay (1-" + strconv.Itoa(len(relays)) + "): ")
|
||||
idx, err := strconv.Atoi(choice)
|
||||
if err != nil || idx < 1 || idx > len(relays) {
|
||||
fmt.Println("❌ Invalid selection")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
selectedRelay := relays[idx-1]
|
||||
fmt.Printf("⏳ Getting relay output options for %s...\n", selectedRelay.Token)
|
||||
|
||||
options, err := c.client.GetRelayOutputOptions(ctx, selectedRelay.Token)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✅ Relay Output Options:")
|
||||
fmt.Printf(" Token: %s\n", options.Token)
|
||||
if len(options.Mode) > 0 {
|
||||
fmt.Println(" Supported Modes:")
|
||||
for _, mode := range options.Mode {
|
||||
fmt.Printf(" - %s\n", mode)
|
||||
}
|
||||
}
|
||||
if len(options.DelayTimes) > 0 {
|
||||
fmt.Println(" Supported Delay Times:")
|
||||
for _, dt := range options.DelayTimes {
|
||||
fmt.Printf(" - %s\n", dt)
|
||||
}
|
||||
}
|
||||
fmt.Printf(" Discrete: %v\n", options.Discrete)
|
||||
}
|
||||
|
||||
func (c *CLI) getVideoOutputConfigurationCLI(ctx context.Context) {
|
||||
// First get available video outputs
|
||||
outputs, err := c.client.GetVideoOutputs(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error getting video outputs: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(outputs) == 0 {
|
||||
fmt.Println("📭 No video outputs available")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Available video outputs:")
|
||||
for i, output := range outputs {
|
||||
fmt.Printf(" %d. %s\n", i+1, output.Token)
|
||||
}
|
||||
|
||||
choice := c.readInput("Select video output (1-" + strconv.Itoa(len(outputs)) + "): ")
|
||||
idx, err := strconv.Atoi(choice)
|
||||
if err != nil || idx < 1 || idx > len(outputs) {
|
||||
fmt.Println("❌ Invalid selection")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
selectedOutput := outputs[idx-1]
|
||||
fmt.Printf("⏳ Getting video output configuration for %s...\n", selectedOutput.Token)
|
||||
|
||||
config, err := c.client.GetVideoOutputConfiguration(ctx, selectedOutput.Token)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✅ Video Output Configuration:")
|
||||
fmt.Printf(" Token: %s\n", config.Token)
|
||||
fmt.Printf(" Name: %s\n", config.Name)
|
||||
fmt.Printf(" Use Count: %d\n", config.UseCount)
|
||||
fmt.Printf(" Output Token: %s\n", config.OutputToken)
|
||||
}
|
||||
|
||||
func (c *CLI) getVideoOutputConfigurationOptionsCLI(ctx context.Context) {
|
||||
// First get available video outputs
|
||||
outputs, err := c.client.GetVideoOutputs(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error getting video outputs: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(outputs) == 0 {
|
||||
fmt.Println("📭 No video outputs available")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Available video outputs:")
|
||||
for i, output := range outputs {
|
||||
fmt.Printf(" %d. %s\n", i+1, output.Token)
|
||||
}
|
||||
|
||||
choice := c.readInput("Select video output (1-" + strconv.Itoa(len(outputs)) + "): ")
|
||||
idx, err := strconv.Atoi(choice)
|
||||
if err != nil || idx < 1 || idx > len(outputs) {
|
||||
fmt.Println("❌ Invalid selection")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
selectedOutput := outputs[idx-1]
|
||||
fmt.Printf("⏳ Getting video output configuration options for %s...\n", selectedOutput.Token)
|
||||
|
||||
options, err := c.client.GetVideoOutputConfigurationOptions(ctx, selectedOutput.Token)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✅ Video Output Configuration Options:")
|
||||
fmt.Printf(" Name Length: Min=%d, Max=%d\n", options.Name.Min, options.Name.Max)
|
||||
if len(options.OutputTokensAvailable) > 0 {
|
||||
fmt.Println(" Available Output Tokens:")
|
||||
for _, token := range options.OutputTokensAvailable {
|
||||
fmt.Printf(" - %s\n", token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+912
@@ -0,0 +1,912 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/0x524a/onvif-go/internal/soap"
|
||||
)
|
||||
|
||||
// Device IO service namespace.
|
||||
const deviceIONamespace = "http://www.onvif.org/ver10/deviceIO/wsdl"
|
||||
|
||||
// Device IO service errors.
|
||||
var (
|
||||
// ErrInvalidDigitalInputToken is returned when digital input token is invalid.
|
||||
ErrInvalidDigitalInputToken = errors.New("invalid digital input token: cannot be empty")
|
||||
// ErrInvalidVideoOutputToken is returned when video output token is invalid.
|
||||
ErrInvalidVideoOutputToken = errors.New("invalid video output token: cannot be empty")
|
||||
// ErrInvalidSerialPortToken is returned when serial port token is invalid.
|
||||
ErrInvalidSerialPortToken = errors.New("invalid serial port token: cannot be empty")
|
||||
// ErrInvalidSerialData is returned when serial data is invalid.
|
||||
ErrInvalidSerialData = errors.New("invalid serial data: cannot be empty")
|
||||
// ErrDigitalInputConfigNil is returned when digital input config is nil.
|
||||
ErrDigitalInputConfigNil = errors.New("digital input config cannot be nil")
|
||||
// ErrSerialPortConfigNil is returned when serial port config is nil.
|
||||
ErrSerialPortConfigNil = errors.New("serial port config cannot be nil")
|
||||
// ErrVideoOutputConfigNil is returned when video output config is nil.
|
||||
ErrVideoOutputConfigNil = errors.New("video output configuration cannot be nil")
|
||||
// ErrInvalidRelayOutputToken is returned when relay output token is invalid.
|
||||
ErrInvalidRelayOutputToken = errors.New("invalid relay output token: cannot be empty")
|
||||
)
|
||||
|
||||
// DeviceIOServiceCapabilities represents the capabilities of the device IO service.
|
||||
type DeviceIOServiceCapabilities struct {
|
||||
VideoSources int
|
||||
VideoOutputs int
|
||||
AudioSources int
|
||||
AudioOutputs int
|
||||
RelayOutputs int
|
||||
SerialPorts int
|
||||
DigitalInputs int
|
||||
DigitalInputOptions bool
|
||||
SerialPortConfiguration bool
|
||||
}
|
||||
|
||||
// DigitalInput represents a digital input.
|
||||
type DigitalInput struct {
|
||||
Token string
|
||||
IdleState DigitalIdleState
|
||||
}
|
||||
|
||||
// DigitalIdleState represents the idle state of a digital input.
|
||||
type DigitalIdleState string
|
||||
|
||||
// Digital idle state constants.
|
||||
const (
|
||||
DigitalIdleOpen DigitalIdleState = "open"
|
||||
DigitalIdleClosed DigitalIdleState = "closed"
|
||||
)
|
||||
|
||||
// VideoOutput represents a video output.
|
||||
type VideoOutput struct {
|
||||
Token string
|
||||
Layout *Layout
|
||||
Resolution *VideoResolution
|
||||
RefreshRate float64
|
||||
AspectRatio string
|
||||
}
|
||||
|
||||
// Layout represents a video output layout.
|
||||
type Layout struct {
|
||||
Pane []PaneLayout
|
||||
Extension interface{}
|
||||
}
|
||||
|
||||
// PaneLayout represents a pane layout.
|
||||
type PaneLayout struct {
|
||||
Pane string
|
||||
Area FloatRectangle
|
||||
}
|
||||
|
||||
// FloatRectangle represents a floating point rectangle.
|
||||
type FloatRectangle struct {
|
||||
Bottom float64
|
||||
Top float64
|
||||
Right float64
|
||||
Left float64
|
||||
}
|
||||
|
||||
// SerialPort represents a serial port.
|
||||
type SerialPort struct {
|
||||
Token string
|
||||
Type SerialPortType
|
||||
}
|
||||
|
||||
// SerialPortType represents the type of a serial port.
|
||||
type SerialPortType string
|
||||
|
||||
// Serial port type constants.
|
||||
const (
|
||||
SerialPortTypeRS232 SerialPortType = "RS232"
|
||||
SerialPortTypeRS422 SerialPortType = "RS422"
|
||||
SerialPortTypeRS485 SerialPortType = "RS485"
|
||||
SerialPortTypeGeneric SerialPortType = "Generic"
|
||||
)
|
||||
|
||||
// SerialPortConfiguration represents a serial port configuration.
|
||||
type SerialPortConfiguration struct {
|
||||
Token string
|
||||
Type SerialPortType
|
||||
BaudRate int
|
||||
ParityBit ParityBit
|
||||
CharacterLength int
|
||||
StopBit float64
|
||||
}
|
||||
|
||||
// ParityBit represents the parity bit setting.
|
||||
type ParityBit string
|
||||
|
||||
// Parity bit constants.
|
||||
const (
|
||||
ParityNone ParityBit = "None"
|
||||
ParityOdd ParityBit = "Odd"
|
||||
ParityEven ParityBit = "Even"
|
||||
ParityMark ParityBit = "Mark"
|
||||
ParitySpace ParityBit = "Space"
|
||||
)
|
||||
|
||||
// SerialPortConfigurationOptions represents serial port configuration options.
|
||||
type SerialPortConfigurationOptions struct {
|
||||
Token string
|
||||
BaudRateList []int
|
||||
ParityBitList []ParityBit
|
||||
CharacterLengthList []int
|
||||
StopBitList []float64
|
||||
}
|
||||
|
||||
// DigitalInputConfigurationOptions represents digital input configuration options.
|
||||
type DigitalInputConfigurationOptions struct {
|
||||
IdleStateOptions []DigitalIdleState
|
||||
}
|
||||
|
||||
// VideoOutputConfiguration represents a video output configuration.
|
||||
type VideoOutputConfiguration struct {
|
||||
Token string
|
||||
Name string
|
||||
UseCount int
|
||||
OutputToken string
|
||||
ForcePersistence bool
|
||||
}
|
||||
|
||||
// VideoOutputConfigurationOptions represents video output configuration options.
|
||||
type VideoOutputConfigurationOptions struct {
|
||||
Name StringRange
|
||||
OutputTokensAvailable []string
|
||||
}
|
||||
|
||||
// StringRange represents a range of string values.
|
||||
type StringRange struct {
|
||||
Min int
|
||||
Max int
|
||||
}
|
||||
|
||||
// RelayOutputOptions represents relay output configuration options.
|
||||
type RelayOutputOptions struct {
|
||||
Token string
|
||||
Mode []RelayMode
|
||||
DelayTimes []string
|
||||
Discrete bool
|
||||
}
|
||||
|
||||
// getDeviceIOEndpoint returns the device IO endpoint.
|
||||
func (c *Client) getDeviceIOEndpoint() string {
|
||||
// Device IO typically uses the main device endpoint.
|
||||
return c.endpoint
|
||||
}
|
||||
|
||||
// GetDeviceIOServiceCapabilities retrieves the capabilities of the device IO service.
|
||||
func (c *Client) GetDeviceIOServiceCapabilities(ctx context.Context) (*DeviceIOServiceCapabilities, error) {
|
||||
endpoint := c.getDeviceIOEndpoint()
|
||||
|
||||
type GetServiceCapabilities struct {
|
||||
XMLName xml.Name `xml:"tmd:GetServiceCapabilities"`
|
||||
Xmlns string `xml:"xmlns:tmd,attr"`
|
||||
}
|
||||
|
||||
type GetServiceCapabilitiesResponse struct {
|
||||
XMLName xml.Name `xml:"GetServiceCapabilitiesResponse"`
|
||||
Capabilities struct {
|
||||
VideoSources int `xml:"VideoSources,attr"`
|
||||
VideoOutputs int `xml:"VideoOutputs,attr"`
|
||||
AudioSources int `xml:"AudioSources,attr"`
|
||||
AudioOutputs int `xml:"AudioOutputs,attr"`
|
||||
RelayOutputs int `xml:"RelayOutputs,attr"`
|
||||
SerialPorts int `xml:"SerialPorts,attr"`
|
||||
DigitalInputs int `xml:"DigitalInputs,attr"`
|
||||
DigitalInputOptions bool `xml:"DigitalInputOptions,attr"`
|
||||
SerialPortConfiguration bool `xml:"SerialPortConfiguration,attr"`
|
||||
} `xml:"Capabilities"`
|
||||
}
|
||||
|
||||
req := GetServiceCapabilities{
|
||||
Xmlns: deviceIONamespace,
|
||||
}
|
||||
|
||||
var resp GetServiceCapabilitiesResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("GetDeviceIOServiceCapabilities failed: %w", err)
|
||||
}
|
||||
|
||||
return &DeviceIOServiceCapabilities{
|
||||
VideoSources: resp.Capabilities.VideoSources,
|
||||
VideoOutputs: resp.Capabilities.VideoOutputs,
|
||||
AudioSources: resp.Capabilities.AudioSources,
|
||||
AudioOutputs: resp.Capabilities.AudioOutputs,
|
||||
RelayOutputs: resp.Capabilities.RelayOutputs,
|
||||
SerialPorts: resp.Capabilities.SerialPorts,
|
||||
DigitalInputs: resp.Capabilities.DigitalInputs,
|
||||
DigitalInputOptions: resp.Capabilities.DigitalInputOptions,
|
||||
SerialPortConfiguration: resp.Capabilities.SerialPortConfiguration,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetDigitalInputs retrieves all digital inputs.
|
||||
func (c *Client) GetDigitalInputs(ctx context.Context) ([]*DigitalInput, error) {
|
||||
endpoint := c.getDeviceIOEndpoint()
|
||||
|
||||
type GetDigitalInputs struct {
|
||||
XMLName xml.Name `xml:"tmd:GetDigitalInputs"`
|
||||
Xmlns string `xml:"xmlns:tmd,attr"`
|
||||
}
|
||||
|
||||
type GetDigitalInputsResponse struct {
|
||||
XMLName xml.Name `xml:"GetDigitalInputsResponse"`
|
||||
DigitalInputs []struct {
|
||||
Token string `xml:"token,attr"`
|
||||
IdleState string `xml:"IdleState,attr"`
|
||||
} `xml:"DigitalInputs"`
|
||||
}
|
||||
|
||||
req := GetDigitalInputs{
|
||||
Xmlns: deviceIONamespace,
|
||||
}
|
||||
|
||||
var resp GetDigitalInputsResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("GetDigitalInputs failed: %w", err)
|
||||
}
|
||||
|
||||
inputs := make([]*DigitalInput, len(resp.DigitalInputs))
|
||||
for i, di := range resp.DigitalInputs {
|
||||
inputs[i] = &DigitalInput{
|
||||
Token: di.Token,
|
||||
IdleState: DigitalIdleState(di.IdleState),
|
||||
}
|
||||
}
|
||||
|
||||
return inputs, nil
|
||||
}
|
||||
|
||||
// GetDigitalInputConfigurationOptions retrieves digital input configuration options.
|
||||
func (c *Client) GetDigitalInputConfigurationOptions(ctx context.Context, token string) (*DigitalInputConfigurationOptions, error) {
|
||||
if token == "" {
|
||||
return nil, ErrInvalidDigitalInputToken
|
||||
}
|
||||
|
||||
endpoint := c.getDeviceIOEndpoint()
|
||||
|
||||
type GetDigitalInputConfigurationOptions struct {
|
||||
XMLName xml.Name `xml:"tmd:GetDigitalInputConfigurationOptions"`
|
||||
Xmlns string `xml:"xmlns:tmd,attr"`
|
||||
Token string `xml:"tmd:Token"`
|
||||
}
|
||||
|
||||
type GetDigitalInputConfigurationOptionsResponse struct {
|
||||
XMLName xml.Name `xml:"GetDigitalInputConfigurationOptionsResponse"`
|
||||
DigitalInputConfigurationOptions struct {
|
||||
IdleState []string `xml:"IdleState"`
|
||||
} `xml:"DigitalInputConfigurationOptions"`
|
||||
}
|
||||
|
||||
req := GetDigitalInputConfigurationOptions{
|
||||
Xmlns: deviceIONamespace,
|
||||
Token: token,
|
||||
}
|
||||
|
||||
var resp GetDigitalInputConfigurationOptionsResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("GetDigitalInputConfigurationOptions failed: %w", err)
|
||||
}
|
||||
|
||||
options := &DigitalInputConfigurationOptions{
|
||||
IdleStateOptions: make([]DigitalIdleState, len(resp.DigitalInputConfigurationOptions.IdleState)),
|
||||
}
|
||||
|
||||
for i, state := range resp.DigitalInputConfigurationOptions.IdleState {
|
||||
options.IdleStateOptions[i] = DigitalIdleState(state)
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
|
||||
// SetDigitalInputConfigurations sets digital input configurations.
|
||||
func (c *Client) SetDigitalInputConfigurations(ctx context.Context, inputs []*DigitalInput) error {
|
||||
if len(inputs) == 0 {
|
||||
return ErrDigitalInputConfigNil
|
||||
}
|
||||
|
||||
endpoint := c.getDeviceIOEndpoint()
|
||||
|
||||
type DigitalInputXML struct {
|
||||
Token string `xml:"token,attr"`
|
||||
IdleState string `xml:"IdleState,attr,omitempty"`
|
||||
}
|
||||
|
||||
type SetDigitalInputConfigurations struct {
|
||||
XMLName xml.Name `xml:"tmd:SetDigitalInputConfigurations"`
|
||||
Xmlns string `xml:"xmlns:tmd,attr"`
|
||||
DigitalInputs []DigitalInputXML `xml:"tmd:DigitalInputs"`
|
||||
}
|
||||
|
||||
type SetDigitalInputConfigurationsResponse struct {
|
||||
XMLName xml.Name `xml:"SetDigitalInputConfigurationsResponse"`
|
||||
}
|
||||
|
||||
digitalInputsXML := make([]DigitalInputXML, len(inputs))
|
||||
for i, input := range inputs {
|
||||
if input.Token == "" {
|
||||
return ErrInvalidDigitalInputToken
|
||||
}
|
||||
|
||||
digitalInputsXML[i] = DigitalInputXML{
|
||||
Token: input.Token,
|
||||
IdleState: string(input.IdleState),
|
||||
}
|
||||
}
|
||||
|
||||
req := SetDigitalInputConfigurations{
|
||||
Xmlns: deviceIONamespace,
|
||||
DigitalInputs: digitalInputsXML,
|
||||
}
|
||||
|
||||
var resp SetDigitalInputConfigurationsResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return fmt.Errorf("SetDigitalInputConfigurations failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVideoOutputs retrieves all video outputs.
|
||||
func (c *Client) GetVideoOutputs(ctx context.Context) ([]*VideoOutput, error) {
|
||||
endpoint := c.getDeviceIOEndpoint()
|
||||
|
||||
type GetVideoOutputs struct {
|
||||
XMLName xml.Name `xml:"tmd:GetVideoOutputs"`
|
||||
Xmlns string `xml:"xmlns:tmd,attr"`
|
||||
}
|
||||
|
||||
type GetVideoOutputsResponse struct {
|
||||
XMLName xml.Name `xml:"GetVideoOutputsResponse"`
|
||||
VideoOutputs []struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Layout *struct {
|
||||
Pane []struct {
|
||||
Pane string `xml:"Pane,attr"`
|
||||
Area struct {
|
||||
Bottom float64 `xml:"bottom,attr"`
|
||||
Top float64 `xml:"top,attr"`
|
||||
Right float64 `xml:"right,attr"`
|
||||
Left float64 `xml:"left,attr"`
|
||||
} `xml:"Area"`
|
||||
} `xml:"Pane"`
|
||||
} `xml:"Layout"`
|
||||
Resolution *struct {
|
||||
Width int `xml:"Width"`
|
||||
Height int `xml:"Height"`
|
||||
} `xml:"Resolution"`
|
||||
RefreshRate float64 `xml:"RefreshRate"`
|
||||
AspectRatio string `xml:"AspectRatio"`
|
||||
} `xml:"VideoOutputs"`
|
||||
}
|
||||
|
||||
req := GetVideoOutputs{
|
||||
Xmlns: deviceIONamespace,
|
||||
}
|
||||
|
||||
var resp GetVideoOutputsResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("GetVideoOutputs failed: %w", err)
|
||||
}
|
||||
|
||||
outputs := make([]*VideoOutput, len(resp.VideoOutputs))
|
||||
for i, vo := range resp.VideoOutputs {
|
||||
output := &VideoOutput{
|
||||
Token: vo.Token,
|
||||
RefreshRate: vo.RefreshRate,
|
||||
AspectRatio: vo.AspectRatio,
|
||||
}
|
||||
|
||||
if vo.Resolution != nil {
|
||||
output.Resolution = &VideoResolution{
|
||||
Width: vo.Resolution.Width,
|
||||
Height: vo.Resolution.Height,
|
||||
}
|
||||
}
|
||||
|
||||
if vo.Layout != nil {
|
||||
output.Layout = &Layout{
|
||||
Pane: make([]PaneLayout, len(vo.Layout.Pane)),
|
||||
}
|
||||
|
||||
for j, pane := range vo.Layout.Pane {
|
||||
output.Layout.Pane[j] = PaneLayout{
|
||||
Pane: pane.Pane,
|
||||
Area: FloatRectangle{
|
||||
Bottom: pane.Area.Bottom,
|
||||
Top: pane.Area.Top,
|
||||
Right: pane.Area.Right,
|
||||
Left: pane.Area.Left,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outputs[i] = output
|
||||
}
|
||||
|
||||
return outputs, nil
|
||||
}
|
||||
|
||||
// GetSerialPorts retrieves all serial ports.
|
||||
func (c *Client) GetSerialPorts(ctx context.Context) ([]*SerialPort, error) {
|
||||
endpoint := c.getDeviceIOEndpoint()
|
||||
|
||||
type GetSerialPorts struct {
|
||||
XMLName xml.Name `xml:"tmd:GetSerialPorts"`
|
||||
Xmlns string `xml:"xmlns:tmd,attr"`
|
||||
}
|
||||
|
||||
type GetSerialPortsResponse struct {
|
||||
XMLName xml.Name `xml:"GetSerialPortsResponse"`
|
||||
SerialPorts []struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Type string `xml:"Type"`
|
||||
} `xml:"SerialPorts"`
|
||||
}
|
||||
|
||||
req := GetSerialPorts{
|
||||
Xmlns: deviceIONamespace,
|
||||
}
|
||||
|
||||
var resp GetSerialPortsResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("GetSerialPorts failed: %w", err)
|
||||
}
|
||||
|
||||
ports := make([]*SerialPort, len(resp.SerialPorts))
|
||||
for i, sp := range resp.SerialPorts {
|
||||
ports[i] = &SerialPort{
|
||||
Token: sp.Token,
|
||||
Type: SerialPortType(sp.Type),
|
||||
}
|
||||
}
|
||||
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
// GetSerialPortConfiguration retrieves a serial port configuration.
|
||||
func (c *Client) GetSerialPortConfiguration(ctx context.Context, serialPortToken string) (*SerialPortConfiguration, error) {
|
||||
if serialPortToken == "" {
|
||||
return nil, ErrInvalidSerialPortToken
|
||||
}
|
||||
|
||||
endpoint := c.getDeviceIOEndpoint()
|
||||
|
||||
type GetSerialPortConfiguration struct {
|
||||
XMLName xml.Name `xml:"tmd:GetSerialPortConfiguration"`
|
||||
Xmlns string `xml:"xmlns:tmd,attr"`
|
||||
SerialPortToken string `xml:"tmd:SerialPortToken"`
|
||||
}
|
||||
|
||||
type GetSerialPortConfigurationResponse struct {
|
||||
XMLName xml.Name `xml:"GetSerialPortConfigurationResponse"`
|
||||
SerialPortConfiguration struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Type string `xml:"Type"`
|
||||
BaudRate int `xml:"BaudRate"`
|
||||
ParityBit string `xml:"ParityBit"`
|
||||
CharacterLength int `xml:"CharacterLength"`
|
||||
StopBit float64 `xml:"StopBit"`
|
||||
} `xml:"SerialPortConfiguration"`
|
||||
}
|
||||
|
||||
req := GetSerialPortConfiguration{
|
||||
Xmlns: deviceIONamespace,
|
||||
SerialPortToken: serialPortToken,
|
||||
}
|
||||
|
||||
var resp GetSerialPortConfigurationResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("GetSerialPortConfiguration failed: %w", err)
|
||||
}
|
||||
|
||||
return &SerialPortConfiguration{
|
||||
Token: resp.SerialPortConfiguration.Token,
|
||||
Type: SerialPortType(resp.SerialPortConfiguration.Type),
|
||||
BaudRate: resp.SerialPortConfiguration.BaudRate,
|
||||
ParityBit: ParityBit(resp.SerialPortConfiguration.ParityBit),
|
||||
CharacterLength: resp.SerialPortConfiguration.CharacterLength,
|
||||
StopBit: resp.SerialPortConfiguration.StopBit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSerialPortConfigurationOptions retrieves serial port configuration options.
|
||||
func (c *Client) GetSerialPortConfigurationOptions(ctx context.Context, serialPortToken string) (*SerialPortConfigurationOptions, error) {
|
||||
if serialPortToken == "" {
|
||||
return nil, ErrInvalidSerialPortToken
|
||||
}
|
||||
|
||||
endpoint := c.getDeviceIOEndpoint()
|
||||
|
||||
type GetSerialPortConfigurationOptions struct {
|
||||
XMLName xml.Name `xml:"tmd:GetSerialPortConfigurationOptions"`
|
||||
Xmlns string `xml:"xmlns:tmd,attr"`
|
||||
SerialPortToken string `xml:"tmd:SerialPortToken"`
|
||||
}
|
||||
|
||||
type GetSerialPortConfigurationOptionsResponse struct {
|
||||
XMLName xml.Name `xml:"GetSerialPortConfigurationOptionsResponse"`
|
||||
SerialPortConfigurationOptions struct {
|
||||
Token string `xml:"token,attr"`
|
||||
BaudRateList []int `xml:"BaudRateList>Items"`
|
||||
ParityBitList []string `xml:"ParityBitList>Items"`
|
||||
CharLengthList []int `xml:"CharacterLengthList>Items"`
|
||||
StopBitList []float64 `xml:"StopBitList>Items"`
|
||||
} `xml:"SerialPortConfigurationOptions"`
|
||||
}
|
||||
|
||||
req := GetSerialPortConfigurationOptions{
|
||||
Xmlns: deviceIONamespace,
|
||||
SerialPortToken: serialPortToken,
|
||||
}
|
||||
|
||||
var resp GetSerialPortConfigurationOptionsResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("GetSerialPortConfigurationOptions failed: %w", err)
|
||||
}
|
||||
|
||||
options := &SerialPortConfigurationOptions{
|
||||
Token: resp.SerialPortConfigurationOptions.Token,
|
||||
BaudRateList: resp.SerialPortConfigurationOptions.BaudRateList,
|
||||
CharacterLengthList: resp.SerialPortConfigurationOptions.CharLengthList,
|
||||
StopBitList: resp.SerialPortConfigurationOptions.StopBitList,
|
||||
}
|
||||
|
||||
// Convert parity bit strings to ParityBit type.
|
||||
options.ParityBitList = make([]ParityBit, len(resp.SerialPortConfigurationOptions.ParityBitList))
|
||||
for i, pb := range resp.SerialPortConfigurationOptions.ParityBitList {
|
||||
options.ParityBitList[i] = ParityBit(pb)
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
|
||||
// SetSerialPortConfiguration sets a serial port configuration.
|
||||
func (c *Client) SetSerialPortConfiguration(ctx context.Context, config *SerialPortConfiguration) error {
|
||||
if config == nil {
|
||||
return ErrSerialPortConfigNil
|
||||
}
|
||||
|
||||
if config.Token == "" {
|
||||
return ErrInvalidSerialPortToken
|
||||
}
|
||||
|
||||
endpoint := c.getDeviceIOEndpoint()
|
||||
|
||||
type SerialPortConfigurationXML struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Type string `xml:"tmd:Type"`
|
||||
BaudRate int `xml:"tmd:BaudRate"`
|
||||
ParityBit string `xml:"tmd:ParityBit"`
|
||||
CharacterLength int `xml:"tmd:CharacterLength"`
|
||||
StopBit float64 `xml:"tmd:StopBit"`
|
||||
}
|
||||
|
||||
type SetSerialPortConfiguration struct {
|
||||
XMLName xml.Name `xml:"tmd:SetSerialPortConfiguration"`
|
||||
Xmlns string `xml:"xmlns:tmd,attr"`
|
||||
SerialPortConfiguration SerialPortConfigurationXML `xml:"tmd:SerialPortConfiguration"`
|
||||
}
|
||||
|
||||
type SetSerialPortConfigurationResponse struct {
|
||||
XMLName xml.Name `xml:"SetSerialPortConfigurationResponse"`
|
||||
}
|
||||
|
||||
req := SetSerialPortConfiguration{
|
||||
Xmlns: deviceIONamespace,
|
||||
SerialPortConfiguration: SerialPortConfigurationXML{
|
||||
Token: config.Token,
|
||||
Type: string(config.Type),
|
||||
BaudRate: config.BaudRate,
|
||||
ParityBit: string(config.ParityBit),
|
||||
CharacterLength: config.CharacterLength,
|
||||
StopBit: config.StopBit,
|
||||
},
|
||||
}
|
||||
|
||||
var resp SetSerialPortConfigurationResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return fmt.Errorf("SetSerialPortConfiguration failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendReceiveSerialCommand sends a serial command and receives a response.
|
||||
func (c *Client) SendReceiveSerialCommand(ctx context.Context, serialPortToken string, data []byte, timeoutSeconds, dataLength int) ([]byte, error) {
|
||||
if serialPortToken == "" {
|
||||
return nil, ErrInvalidSerialPortToken
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return nil, ErrInvalidSerialData
|
||||
}
|
||||
|
||||
endpoint := c.getDeviceIOEndpoint()
|
||||
|
||||
type SerialData struct {
|
||||
Binary string `xml:"tt:Binary,omitempty"`
|
||||
}
|
||||
|
||||
type SendReceiveSerialCommand struct {
|
||||
XMLName xml.Name `xml:"tmd:SendReceiveSerialCommand"`
|
||||
Xmlns string `xml:"xmlns:tmd,attr"`
|
||||
XmlnsTT string `xml:"xmlns:tt,attr"`
|
||||
Token string `xml:"tmd:Token"`
|
||||
SerialData SerialData `xml:"tmd:SerialData"`
|
||||
TimeOut string `xml:"tmd:TimeOut,omitempty"`
|
||||
DataLength int `xml:"tmd:DataLength,omitempty"`
|
||||
}
|
||||
|
||||
type SendReceiveSerialCommandResponse struct {
|
||||
XMLName xml.Name `xml:"SendReceiveSerialCommandResponse"`
|
||||
SerialData struct {
|
||||
Binary string `xml:"Binary"`
|
||||
} `xml:"SerialData"`
|
||||
}
|
||||
|
||||
req := SendReceiveSerialCommand{
|
||||
Xmlns: deviceIONamespace,
|
||||
XmlnsTT: "http://www.onvif.org/ver10/schema",
|
||||
Token: serialPortToken,
|
||||
SerialData: SerialData{
|
||||
Binary: string(data),
|
||||
},
|
||||
DataLength: dataLength,
|
||||
}
|
||||
|
||||
if timeoutSeconds > 0 {
|
||||
req.TimeOut = fmt.Sprintf("PT%dS", timeoutSeconds)
|
||||
}
|
||||
|
||||
var resp SendReceiveSerialCommandResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("SendReceiveSerialCommand failed: %w", err)
|
||||
}
|
||||
|
||||
return []byte(resp.SerialData.Binary), nil
|
||||
}
|
||||
|
||||
// GetVideoOutputConfiguration retrieves a video output configuration.
|
||||
func (c *Client) GetVideoOutputConfiguration(ctx context.Context, videoOutputToken string) (*VideoOutputConfiguration, error) {
|
||||
if videoOutputToken == "" {
|
||||
return nil, ErrInvalidVideoOutputToken
|
||||
}
|
||||
|
||||
endpoint := c.getDeviceIOEndpoint()
|
||||
|
||||
type GetVideoOutputConfiguration struct {
|
||||
XMLName xml.Name `xml:"tmd:GetVideoOutputConfiguration"`
|
||||
Xmlns string `xml:"xmlns:tmd,attr"`
|
||||
VideoOutputToken string `xml:"tmd:VideoOutputToken"`
|
||||
}
|
||||
|
||||
type GetVideoOutputConfigurationResponse struct {
|
||||
XMLName xml.Name `xml:"GetVideoOutputConfigurationResponse"`
|
||||
VideoOutputConfiguration struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Name string `xml:"Name"`
|
||||
UseCount int `xml:"UseCount"`
|
||||
OutputToken string `xml:"OutputToken"`
|
||||
} `xml:"VideoOutputConfiguration"`
|
||||
}
|
||||
|
||||
req := GetVideoOutputConfiguration{
|
||||
Xmlns: deviceIONamespace,
|
||||
VideoOutputToken: videoOutputToken,
|
||||
}
|
||||
|
||||
var resp GetVideoOutputConfigurationResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("GetVideoOutputConfiguration failed: %w", err)
|
||||
}
|
||||
|
||||
return &VideoOutputConfiguration{
|
||||
Token: resp.VideoOutputConfiguration.Token,
|
||||
Name: resp.VideoOutputConfiguration.Name,
|
||||
UseCount: resp.VideoOutputConfiguration.UseCount,
|
||||
OutputToken: resp.VideoOutputConfiguration.OutputToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetVideoOutputConfigurationOptions retrieves video output configuration options.
|
||||
func (c *Client) GetVideoOutputConfigurationOptions(ctx context.Context, videoOutputToken string) (*VideoOutputConfigurationOptions, error) {
|
||||
if videoOutputToken == "" {
|
||||
return nil, ErrInvalidVideoOutputToken
|
||||
}
|
||||
|
||||
endpoint := c.getDeviceIOEndpoint()
|
||||
|
||||
type GetVideoOutputConfigurationOptions struct {
|
||||
XMLName xml.Name `xml:"tmd:GetVideoOutputConfigurationOptions"`
|
||||
Xmlns string `xml:"xmlns:tmd,attr"`
|
||||
VideoOutputToken string `xml:"tmd:VideoOutputToken"`
|
||||
}
|
||||
|
||||
type GetVideoOutputConfigurationOptionsResponse struct {
|
||||
XMLName xml.Name `xml:"GetVideoOutputConfigurationOptionsResponse"`
|
||||
VideoOutputConfigurationOptions struct {
|
||||
Name struct {
|
||||
Min int `xml:"Min,attr"`
|
||||
Max int `xml:"Max,attr"`
|
||||
} `xml:"Name"`
|
||||
OutputTokensAvailable []string `xml:"OutputTokensAvailable"`
|
||||
} `xml:"VideoOutputConfigurationOptions"`
|
||||
}
|
||||
|
||||
req := GetVideoOutputConfigurationOptions{
|
||||
Xmlns: deviceIONamespace,
|
||||
VideoOutputToken: videoOutputToken,
|
||||
}
|
||||
|
||||
var resp GetVideoOutputConfigurationOptionsResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("GetVideoOutputConfigurationOptions failed: %w", err)
|
||||
}
|
||||
|
||||
return &VideoOutputConfigurationOptions{
|
||||
Name: StringRange{
|
||||
Min: resp.VideoOutputConfigurationOptions.Name.Min,
|
||||
Max: resp.VideoOutputConfigurationOptions.Name.Max,
|
||||
},
|
||||
OutputTokensAvailable: resp.VideoOutputConfigurationOptions.OutputTokensAvailable,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetVideoOutputConfiguration sets a video output configuration.
|
||||
func (c *Client) SetVideoOutputConfiguration(ctx context.Context, config *VideoOutputConfiguration) error {
|
||||
if config == nil {
|
||||
return ErrVideoOutputConfigNil
|
||||
}
|
||||
|
||||
if config.Token == "" {
|
||||
return ErrInvalidVideoOutputToken
|
||||
}
|
||||
|
||||
endpoint := c.getDeviceIOEndpoint()
|
||||
|
||||
type VideoOutputConfigurationXML struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Name string `xml:"tt:Name"`
|
||||
UseCount int `xml:"tt:UseCount"`
|
||||
OutputToken string `xml:"tt:OutputToken"`
|
||||
}
|
||||
|
||||
type SetVideoOutputConfiguration struct {
|
||||
XMLName xml.Name `xml:"tmd:SetVideoOutputConfiguration"`
|
||||
Xmlns string `xml:"xmlns:tmd,attr"`
|
||||
XmlnsTT string `xml:"xmlns:tt,attr"`
|
||||
Configuration VideoOutputConfigurationXML `xml:"tmd:Configuration"`
|
||||
ForcePersistence bool `xml:"tmd:ForcePersistence"`
|
||||
}
|
||||
|
||||
type SetVideoOutputConfigurationResponse struct {
|
||||
XMLName xml.Name `xml:"SetVideoOutputConfigurationResponse"`
|
||||
}
|
||||
|
||||
req := SetVideoOutputConfiguration{
|
||||
Xmlns: deviceIONamespace,
|
||||
XmlnsTT: "http://www.onvif.org/ver10/schema",
|
||||
Configuration: VideoOutputConfigurationXML{
|
||||
Token: config.Token,
|
||||
Name: config.Name,
|
||||
UseCount: config.UseCount,
|
||||
OutputToken: config.OutputToken,
|
||||
},
|
||||
ForcePersistence: config.ForcePersistence,
|
||||
}
|
||||
|
||||
var resp SetVideoOutputConfigurationResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return fmt.Errorf("SetVideoOutputConfiguration failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRelayOutputOptions retrieves relay output options.
|
||||
func (c *Client) GetRelayOutputOptions(ctx context.Context, relayOutputToken string) (*RelayOutputOptions, error) {
|
||||
if relayOutputToken == "" {
|
||||
return nil, ErrInvalidRelayOutputToken
|
||||
}
|
||||
|
||||
endpoint := c.getDeviceIOEndpoint()
|
||||
|
||||
type GetRelayOutputOptions struct {
|
||||
XMLName xml.Name `xml:"tmd:GetRelayOutputOptions"`
|
||||
Xmlns string `xml:"xmlns:tmd,attr"`
|
||||
RelayOutputToken string `xml:"tmd:RelayOutputToken"`
|
||||
}
|
||||
|
||||
type GetRelayOutputOptionsResponse struct {
|
||||
XMLName xml.Name `xml:"GetRelayOutputOptionsResponse"`
|
||||
RelayOutputOptions struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Mode []string `xml:"Mode"`
|
||||
DelayTimes []string `xml:"DelayTimes"`
|
||||
Discrete bool `xml:"Discrete"`
|
||||
} `xml:"RelayOutputOptions"`
|
||||
}
|
||||
|
||||
req := GetRelayOutputOptions{
|
||||
Xmlns: deviceIONamespace,
|
||||
RelayOutputToken: relayOutputToken,
|
||||
}
|
||||
|
||||
var resp GetRelayOutputOptionsResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("GetRelayOutputOptions failed: %w", err)
|
||||
}
|
||||
|
||||
modes := make([]RelayMode, len(resp.RelayOutputOptions.Mode))
|
||||
for i, m := range resp.RelayOutputOptions.Mode {
|
||||
modes[i] = RelayMode(m)
|
||||
}
|
||||
|
||||
return &RelayOutputOptions{
|
||||
Token: resp.RelayOutputOptions.Token,
|
||||
Mode: modes,
|
||||
DelayTimes: resp.RelayOutputOptions.DelayTimes,
|
||||
Discrete: resp.RelayOutputOptions.Discrete,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,922 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testDeviceIOXMLHeader = `<?xml version="1.0" encoding="UTF-8"?>`
|
||||
|
||||
func newMockDeviceIOServer() *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/soap+xml")
|
||||
|
||||
body := make([]byte, r.ContentLength)
|
||||
_, _ = r.Body.Read(body)
|
||||
bodyStr := string(body)
|
||||
|
||||
var response string
|
||||
|
||||
switch {
|
||||
case strings.Contains(bodyStr, "GetServiceCapabilities") && strings.Contains(bodyStr, "deviceIO"):
|
||||
response = testDeviceIOXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tmd:GetServiceCapabilitiesResponse xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl">
|
||||
<tmd:Capabilities
|
||||
VideoSources="4"
|
||||
VideoOutputs="2"
|
||||
AudioSources="2"
|
||||
AudioOutputs="2"
|
||||
RelayOutputs="4"
|
||||
SerialPorts="2"
|
||||
DigitalInputs="8"
|
||||
DigitalInputOptions="true"
|
||||
SerialPortConfiguration="true"/>
|
||||
</tmd:GetServiceCapabilitiesResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "GetDigitalInputConfigurationOptions"):
|
||||
response = testDeviceIOXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tmd:GetDigitalInputConfigurationOptionsResponse xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl">
|
||||
<tmd:DigitalInputConfigurationOptions>
|
||||
<tmd:IdleState>open</tmd:IdleState>
|
||||
<tmd:IdleState>closed</tmd:IdleState>
|
||||
</tmd:DigitalInputConfigurationOptions>
|
||||
</tmd:GetDigitalInputConfigurationOptionsResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "GetDigitalInputs"):
|
||||
response = testDeviceIOXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tmd:GetDigitalInputsResponse xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl">
|
||||
<tmd:DigitalInputs token="input_001" IdleState="open"/>
|
||||
<tmd:DigitalInputs token="input_002" IdleState="closed"/>
|
||||
</tmd:GetDigitalInputsResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "SetDigitalInputConfigurations"):
|
||||
response = testDeviceIOXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tmd:SetDigitalInputConfigurationsResponse xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl"/>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "GetVideoOutputs"):
|
||||
response = testDeviceIOXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tmd:GetVideoOutputsResponse xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl">
|
||||
<tmd:VideoOutputs token="video_out_001">
|
||||
<tmd:Layout>
|
||||
<tt:Pane xmlns:tt="http://www.onvif.org/ver10/schema" Pane="main">
|
||||
<tt:Area bottom="1.0" top="0.0" right="1.0" left="0.0"/>
|
||||
</tt:Pane>
|
||||
</tmd:Layout>
|
||||
<tmd:Resolution>
|
||||
<tmd:Width>1920</tmd:Width>
|
||||
<tmd:Height>1080</tmd:Height>
|
||||
</tmd:Resolution>
|
||||
<tmd:RefreshRate>60.0</tmd:RefreshRate>
|
||||
<tmd:AspectRatio>16:9</tmd:AspectRatio>
|
||||
</tmd:VideoOutputs>
|
||||
</tmd:GetVideoOutputsResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "GetSerialPortConfigurationOptions"):
|
||||
response = testDeviceIOXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tmd:GetSerialPortConfigurationOptionsResponse xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl">
|
||||
<tmd:SerialPortConfigurationOptions token="serial_001">
|
||||
<tmd:BaudRateList><tmd:Items>9600</tmd:Items><tmd:Items>19200</tmd:Items><tmd:Items>38400</tmd:Items></tmd:BaudRateList>
|
||||
<tmd:ParityBitList><tmd:Items>None</tmd:Items><tmd:Items>Odd</tmd:Items><tmd:Items>Even</tmd:Items></tmd:ParityBitList>
|
||||
<tmd:CharacterLengthList><tmd:Items>7</tmd:Items><tmd:Items>8</tmd:Items></tmd:CharacterLengthList>
|
||||
<tmd:StopBitList><tmd:Items>1</tmd:Items><tmd:Items>2</tmd:Items></tmd:StopBitList>
|
||||
</tmd:SerialPortConfigurationOptions>
|
||||
</tmd:GetSerialPortConfigurationOptionsResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "GetSerialPortConfiguration"):
|
||||
response = testDeviceIOXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tmd:GetSerialPortConfigurationResponse xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl">
|
||||
<tmd:SerialPortConfiguration token="serial_001">
|
||||
<tmd:Type>RS232</tmd:Type>
|
||||
<tmd:BaudRate>9600</tmd:BaudRate>
|
||||
<tmd:ParityBit>None</tmd:ParityBit>
|
||||
<tmd:CharacterLength>8</tmd:CharacterLength>
|
||||
<tmd:StopBit>1</tmd:StopBit>
|
||||
</tmd:SerialPortConfiguration>
|
||||
</tmd:GetSerialPortConfigurationResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "GetSerialPorts"):
|
||||
response = testDeviceIOXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tmd:GetSerialPortsResponse xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl">
|
||||
<tmd:SerialPorts token="serial_001">
|
||||
<tmd:Type>RS232</tmd:Type>
|
||||
</tmd:SerialPorts>
|
||||
<tmd:SerialPorts token="serial_002">
|
||||
<tmd:Type>RS485</tmd:Type>
|
||||
</tmd:SerialPorts>
|
||||
</tmd:GetSerialPortsResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "SetSerialPortConfiguration"):
|
||||
response = testDeviceIOXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tmd:SetSerialPortConfigurationResponse xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl"/>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "SendReceiveSerialCommand"):
|
||||
response = testDeviceIOXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tmd:SendReceiveSerialCommandResponse xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl">
|
||||
<tmd:SerialData>
|
||||
<tt:Binary xmlns:tt="http://www.onvif.org/ver10/schema">OK</tt:Binary>
|
||||
</tmd:SerialData>
|
||||
</tmd:SendReceiveSerialCommandResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "GetVideoOutputConfigurationOptions"):
|
||||
response = testDeviceIOXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tmd:GetVideoOutputConfigurationOptionsResponse xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl">
|
||||
<tmd:VideoOutputConfigurationOptions>
|
||||
<tmd:Name Min="1" Max="64"/>
|
||||
<tmd:OutputTokensAvailable>video_out_001</tmd:OutputTokensAvailable>
|
||||
<tmd:OutputTokensAvailable>video_out_002</tmd:OutputTokensAvailable>
|
||||
</tmd:VideoOutputConfigurationOptions>
|
||||
</tmd:GetVideoOutputConfigurationOptionsResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "GetVideoOutputConfiguration"):
|
||||
response = testDeviceIOXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tmd:GetVideoOutputConfigurationResponse xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl">
|
||||
<tmd:VideoOutputConfiguration token="config_001">
|
||||
<tmd:Name>Main Output</tmd:Name>
|
||||
<tmd:UseCount>2</tmd:UseCount>
|
||||
<tmd:OutputToken>video_out_001</tmd:OutputToken>
|
||||
</tmd:VideoOutputConfiguration>
|
||||
</tmd:GetVideoOutputConfigurationResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "SetVideoOutputConfiguration"):
|
||||
response = testDeviceIOXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tmd:SetVideoOutputConfigurationResponse xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl"/>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "GetRelayOutputOptions"):
|
||||
response = testDeviceIOXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tmd:GetRelayOutputOptionsResponse xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl">
|
||||
<tmd:RelayOutputOptions token="relay_001">
|
||||
<tmd:Mode>Monostable</tmd:Mode>
|
||||
<tmd:Mode>Bistable</tmd:Mode>
|
||||
<tmd:DelayTimes>PT1S</tmd:DelayTimes>
|
||||
<tmd:DelayTimes>PT5S</tmd:DelayTimes>
|
||||
<tmd:DelayTimes>PT10S</tmd:DelayTimes>
|
||||
<tmd:Discrete>true</tmd:Discrete>
|
||||
</tmd:RelayOutputOptions>
|
||||
</tmd:GetRelayOutputOptionsResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
default:
|
||||
response = testDeviceIOXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<SOAP-ENV:Fault>
|
||||
<SOAP-ENV:Code><SOAP-ENV:Value>SOAP-ENV:Receiver</SOAP-ENV:Value></SOAP-ENV:Code>
|
||||
<SOAP-ENV:Reason><SOAP-ENV:Text>Unknown action</SOAP-ENV:Text></SOAP-ENV:Reason>
|
||||
</SOAP-ENV:Fault>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte(response))
|
||||
}))
|
||||
}
|
||||
|
||||
func TestGetDeviceIOServiceCapabilities(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
caps, err := client.GetDeviceIOServiceCapabilities(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDeviceIOServiceCapabilities failed: %v", err)
|
||||
}
|
||||
|
||||
if caps.VideoSources != 4 {
|
||||
t.Errorf("Expected VideoSources to be 4, got %d", caps.VideoSources)
|
||||
}
|
||||
|
||||
if caps.VideoOutputs != 2 {
|
||||
t.Errorf("Expected VideoOutputs to be 2, got %d", caps.VideoOutputs)
|
||||
}
|
||||
|
||||
if caps.AudioSources != 2 {
|
||||
t.Errorf("Expected AudioSources to be 2, got %d", caps.AudioSources)
|
||||
}
|
||||
|
||||
if caps.AudioOutputs != 2 {
|
||||
t.Errorf("Expected AudioOutputs to be 2, got %d", caps.AudioOutputs)
|
||||
}
|
||||
|
||||
if caps.RelayOutputs != 4 {
|
||||
t.Errorf("Expected RelayOutputs to be 4, got %d", caps.RelayOutputs)
|
||||
}
|
||||
|
||||
if caps.SerialPorts != 2 {
|
||||
t.Errorf("Expected SerialPorts to be 2, got %d", caps.SerialPorts)
|
||||
}
|
||||
|
||||
if caps.DigitalInputs != 8 {
|
||||
t.Errorf("Expected DigitalInputs to be 8, got %d", caps.DigitalInputs)
|
||||
}
|
||||
|
||||
if !caps.DigitalInputOptions {
|
||||
t.Error("Expected DigitalInputOptions to be true")
|
||||
}
|
||||
|
||||
if !caps.SerialPortConfiguration {
|
||||
t.Error("Expected SerialPortConfiguration to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDigitalInputs(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
inputs, err := client.GetDigitalInputs(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDigitalInputs failed: %v", err)
|
||||
}
|
||||
|
||||
if len(inputs) != 2 {
|
||||
t.Fatalf("Expected 2 digital inputs, got %d", len(inputs))
|
||||
}
|
||||
|
||||
if inputs[0].Token != "input_001" {
|
||||
t.Errorf("Expected first input token 'input_001', got '%s'", inputs[0].Token)
|
||||
}
|
||||
|
||||
if inputs[0].IdleState != DigitalIdleOpen {
|
||||
t.Errorf("Expected first input idle state 'open', got '%s'", inputs[0].IdleState)
|
||||
}
|
||||
|
||||
if inputs[1].IdleState != DigitalIdleClosed {
|
||||
t.Errorf("Expected second input idle state 'closed', got '%s'", inputs[1].IdleState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDigitalInputConfigurationOptions(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
options, err := client.GetDigitalInputConfigurationOptions(ctx, "input_001")
|
||||
if err != nil {
|
||||
t.Fatalf("GetDigitalInputConfigurationOptions failed: %v", err)
|
||||
}
|
||||
|
||||
if len(options.IdleStateOptions) != 2 {
|
||||
t.Errorf("Expected 2 idle state options, got %d", len(options.IdleStateOptions))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDigitalInputConfigurationOptionsInvalidToken(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
_, err = client.GetDigitalInputConfigurationOptions(ctx, "")
|
||||
if !errors.Is(err, ErrInvalidDigitalInputToken) {
|
||||
t.Errorf("Expected ErrInvalidDigitalInputToken, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDigitalInputConfigurations(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
inputs := []*DigitalInput{
|
||||
{Token: "input_001", IdleState: DigitalIdleOpen},
|
||||
{Token: "input_002", IdleState: DigitalIdleClosed},
|
||||
}
|
||||
|
||||
err = client.SetDigitalInputConfigurations(ctx, inputs)
|
||||
if err != nil {
|
||||
t.Fatalf("SetDigitalInputConfigurations failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDigitalInputConfigurationsValidation(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test empty inputs.
|
||||
err = client.SetDigitalInputConfigurations(ctx, []*DigitalInput{})
|
||||
if !errors.Is(err, ErrDigitalInputConfigNil) {
|
||||
t.Errorf("Expected ErrDigitalInputConfigNil, got %v", err)
|
||||
}
|
||||
|
||||
// Test input with empty token.
|
||||
inputs := []*DigitalInput{{Token: "", IdleState: DigitalIdleOpen}}
|
||||
err = client.SetDigitalInputConfigurations(ctx, inputs)
|
||||
if !errors.Is(err, ErrInvalidDigitalInputToken) {
|
||||
t.Errorf("Expected ErrInvalidDigitalInputToken, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVideoOutputs(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
outputs, err := client.GetVideoOutputs(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetVideoOutputs failed: %v", err)
|
||||
}
|
||||
|
||||
if len(outputs) != 1 {
|
||||
t.Fatalf("Expected 1 video output, got %d", len(outputs))
|
||||
}
|
||||
|
||||
if outputs[0].Token != "video_out_001" {
|
||||
t.Errorf("Expected video output token 'video_out_001', got '%s'", outputs[0].Token)
|
||||
}
|
||||
|
||||
if outputs[0].Resolution == nil {
|
||||
t.Fatal("Expected Resolution to be set")
|
||||
}
|
||||
|
||||
if outputs[0].Resolution.Width != 1920 {
|
||||
t.Errorf("Expected resolution width 1920, got %d", outputs[0].Resolution.Width)
|
||||
}
|
||||
|
||||
if outputs[0].Resolution.Height != 1080 {
|
||||
t.Errorf("Expected resolution height 1080, got %d", outputs[0].Resolution.Height)
|
||||
}
|
||||
|
||||
if outputs[0].RefreshRate != 60.0 {
|
||||
t.Errorf("Expected refresh rate 60.0, got %f", outputs[0].RefreshRate)
|
||||
}
|
||||
|
||||
if outputs[0].AspectRatio != "16:9" {
|
||||
t.Errorf("Expected aspect ratio '16:9', got '%s'", outputs[0].AspectRatio)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSerialPorts(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ports, err := client.GetSerialPorts(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSerialPorts failed: %v", err)
|
||||
}
|
||||
|
||||
if len(ports) != 2 {
|
||||
t.Fatalf("Expected 2 serial ports, got %d", len(ports))
|
||||
}
|
||||
|
||||
if ports[0].Token != "serial_001" {
|
||||
t.Errorf("Expected first serial port token 'serial_001', got '%s'", ports[0].Token)
|
||||
}
|
||||
|
||||
if ports[0].Type != SerialPortTypeRS232 {
|
||||
t.Errorf("Expected first serial port type RS232, got '%s'", ports[0].Type)
|
||||
}
|
||||
|
||||
if ports[1].Type != SerialPortTypeRS485 {
|
||||
t.Errorf("Expected second serial port type RS485, got '%s'", ports[1].Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSerialPortConfiguration(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
config, err := client.GetSerialPortConfiguration(ctx, "serial_001")
|
||||
if err != nil {
|
||||
t.Fatalf("GetSerialPortConfiguration failed: %v", err)
|
||||
}
|
||||
|
||||
if config.Token != "serial_001" {
|
||||
t.Errorf("Expected token 'serial_001', got '%s'", config.Token)
|
||||
}
|
||||
|
||||
if config.Type != SerialPortTypeRS232 {
|
||||
t.Errorf("Expected type RS232, got '%s'", config.Type)
|
||||
}
|
||||
|
||||
if config.BaudRate != 9600 {
|
||||
t.Errorf("Expected baud rate 9600, got %d", config.BaudRate)
|
||||
}
|
||||
|
||||
if config.ParityBit != ParityNone {
|
||||
t.Errorf("Expected parity None, got '%s'", config.ParityBit)
|
||||
}
|
||||
|
||||
if config.CharacterLength != 8 {
|
||||
t.Errorf("Expected character length 8, got %d", config.CharacterLength)
|
||||
}
|
||||
|
||||
if config.StopBit != 1 {
|
||||
t.Errorf("Expected stop bit 1, got %f", config.StopBit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSerialPortConfigurationInvalidToken(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
_, err = client.GetSerialPortConfiguration(ctx, "")
|
||||
if !errors.Is(err, ErrInvalidSerialPortToken) {
|
||||
t.Errorf("Expected ErrInvalidSerialPortToken, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSerialPortConfigurationOptions(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
options, err := client.GetSerialPortConfigurationOptions(ctx, "serial_001")
|
||||
if err != nil {
|
||||
t.Fatalf("GetSerialPortConfigurationOptions failed: %v", err)
|
||||
}
|
||||
|
||||
if len(options.BaudRateList) != 3 {
|
||||
t.Errorf("Expected 3 baud rate options, got %d", len(options.BaudRateList))
|
||||
}
|
||||
|
||||
if len(options.ParityBitList) != 3 {
|
||||
t.Errorf("Expected 3 parity bit options, got %d", len(options.ParityBitList))
|
||||
}
|
||||
|
||||
if len(options.CharacterLengthList) != 2 {
|
||||
t.Errorf("Expected 2 character length options, got %d", len(options.CharacterLengthList))
|
||||
}
|
||||
|
||||
if len(options.StopBitList) != 2 {
|
||||
t.Errorf("Expected 2 stop bit options, got %d", len(options.StopBitList))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSerialPortConfigurationOptionsInvalidToken(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
_, err = client.GetSerialPortConfigurationOptions(ctx, "")
|
||||
if !errors.Is(err, ErrInvalidSerialPortToken) {
|
||||
t.Errorf("Expected ErrInvalidSerialPortToken, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSerialPortConfiguration(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
config := &SerialPortConfiguration{
|
||||
Token: "serial_001",
|
||||
Type: SerialPortTypeRS232,
|
||||
BaudRate: 19200,
|
||||
ParityBit: ParityNone,
|
||||
CharacterLength: 8,
|
||||
StopBit: 1,
|
||||
}
|
||||
|
||||
err = client.SetSerialPortConfiguration(ctx, config)
|
||||
if err != nil {
|
||||
t.Fatalf("SetSerialPortConfiguration failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSerialPortConfigurationValidation(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test nil config.
|
||||
err = client.SetSerialPortConfiguration(ctx, nil)
|
||||
if !errors.Is(err, ErrSerialPortConfigNil) {
|
||||
t.Errorf("Expected ErrSerialPortConfigNil, got %v", err)
|
||||
}
|
||||
|
||||
// Test empty token.
|
||||
config := &SerialPortConfiguration{Token: ""}
|
||||
err = client.SetSerialPortConfiguration(ctx, config)
|
||||
if !errors.Is(err, ErrInvalidSerialPortToken) {
|
||||
t.Errorf("Expected ErrInvalidSerialPortToken, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendReceiveSerialCommand(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
response, err := client.SendReceiveSerialCommand(ctx, "serial_001", []byte("HELLO"), 5, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("SendReceiveSerialCommand failed: %v", err)
|
||||
}
|
||||
|
||||
if string(response) != "OK" {
|
||||
t.Errorf("Expected response 'OK', got '%s'", string(response))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendReceiveSerialCommandValidation(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test empty token.
|
||||
_, err = client.SendReceiveSerialCommand(ctx, "", []byte("HELLO"), 5, 10)
|
||||
if !errors.Is(err, ErrInvalidSerialPortToken) {
|
||||
t.Errorf("Expected ErrInvalidSerialPortToken, got %v", err)
|
||||
}
|
||||
|
||||
// Test empty data.
|
||||
_, err = client.SendReceiveSerialCommand(ctx, "serial_001", []byte{}, 5, 10)
|
||||
if !errors.Is(err, ErrInvalidSerialData) {
|
||||
t.Errorf("Expected ErrInvalidSerialData, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigitalIdleStateConstants(t *testing.T) {
|
||||
if DigitalIdleOpen != "open" {
|
||||
t.Errorf("DigitalIdleOpen should be 'open'")
|
||||
}
|
||||
|
||||
if DigitalIdleClosed != "closed" {
|
||||
t.Errorf("DigitalIdleClosed should be 'closed'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSerialPortTypeConstants(t *testing.T) {
|
||||
if SerialPortTypeRS232 != "RS232" {
|
||||
t.Errorf("SerialPortTypeRS232 should be 'RS232'")
|
||||
}
|
||||
|
||||
if SerialPortTypeRS422 != "RS422" {
|
||||
t.Errorf("SerialPortTypeRS422 should be 'RS422'")
|
||||
}
|
||||
|
||||
if SerialPortTypeRS485 != "RS485" {
|
||||
t.Errorf("SerialPortTypeRS485 should be 'RS485'")
|
||||
}
|
||||
|
||||
if SerialPortTypeGeneric != "Generic" {
|
||||
t.Errorf("SerialPortTypeGeneric should be 'Generic'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParityBitConstants(t *testing.T) {
|
||||
if ParityNone != "None" {
|
||||
t.Errorf("ParityNone should be 'None'")
|
||||
}
|
||||
|
||||
if ParityOdd != "Odd" {
|
||||
t.Errorf("ParityOdd should be 'Odd'")
|
||||
}
|
||||
|
||||
if ParityEven != "Even" {
|
||||
t.Errorf("ParityEven should be 'Even'")
|
||||
}
|
||||
|
||||
if ParityMark != "Mark" {
|
||||
t.Errorf("ParityMark should be 'Mark'")
|
||||
}
|
||||
|
||||
if ParitySpace != "Space" {
|
||||
t.Errorf("ParitySpace should be 'Space'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVideoOutputConfiguration(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
config, err := client.GetVideoOutputConfiguration(ctx, "video_out_001")
|
||||
if err != nil {
|
||||
t.Fatalf("GetVideoOutputConfiguration failed: %v", err)
|
||||
}
|
||||
|
||||
if config.Token != "config_001" {
|
||||
t.Errorf("Expected token 'config_001', got '%s'", config.Token)
|
||||
}
|
||||
|
||||
if config.Name != "Main Output" {
|
||||
t.Errorf("Expected name 'Main Output', got '%s'", config.Name)
|
||||
}
|
||||
|
||||
if config.UseCount != 2 {
|
||||
t.Errorf("Expected use count 2, got %d", config.UseCount)
|
||||
}
|
||||
|
||||
if config.OutputToken != "video_out_001" {
|
||||
t.Errorf("Expected output token 'video_out_001', got '%s'", config.OutputToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVideoOutputConfigurationInvalidToken(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
_, err = client.GetVideoOutputConfiguration(ctx, "")
|
||||
if !errors.Is(err, ErrInvalidVideoOutputToken) {
|
||||
t.Errorf("Expected ErrInvalidVideoOutputToken, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVideoOutputConfigurationOptions(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
options, err := client.GetVideoOutputConfigurationOptions(ctx, "video_out_001")
|
||||
if err != nil {
|
||||
t.Fatalf("GetVideoOutputConfigurationOptions failed: %v", err)
|
||||
}
|
||||
|
||||
if options.Name.Min != 1 {
|
||||
t.Errorf("Expected Name.Min to be 1, got %d", options.Name.Min)
|
||||
}
|
||||
|
||||
if options.Name.Max != 64 {
|
||||
t.Errorf("Expected Name.Max to be 64, got %d", options.Name.Max)
|
||||
}
|
||||
|
||||
if len(options.OutputTokensAvailable) != 2 {
|
||||
t.Errorf("Expected 2 output tokens available, got %d", len(options.OutputTokensAvailable))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVideoOutputConfigurationOptionsInvalidToken(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
_, err = client.GetVideoOutputConfigurationOptions(ctx, "")
|
||||
if !errors.Is(err, ErrInvalidVideoOutputToken) {
|
||||
t.Errorf("Expected ErrInvalidVideoOutputToken, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetVideoOutputConfiguration(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
config := &VideoOutputConfiguration{
|
||||
Token: "config_001",
|
||||
Name: "Main Output",
|
||||
UseCount: 2,
|
||||
OutputToken: "video_out_001",
|
||||
ForcePersistence: true,
|
||||
}
|
||||
|
||||
err = client.SetVideoOutputConfiguration(ctx, config)
|
||||
if err != nil {
|
||||
t.Fatalf("SetVideoOutputConfiguration failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetVideoOutputConfigurationValidation(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test nil config.
|
||||
err = client.SetVideoOutputConfiguration(ctx, nil)
|
||||
if !errors.Is(err, ErrVideoOutputConfigNil) {
|
||||
t.Errorf("Expected ErrVideoOutputConfigNil, got %v", err)
|
||||
}
|
||||
|
||||
// Test empty token.
|
||||
config := &VideoOutputConfiguration{Token: ""}
|
||||
err = client.SetVideoOutputConfiguration(ctx, config)
|
||||
if !errors.Is(err, ErrInvalidVideoOutputToken) {
|
||||
t.Errorf("Expected ErrInvalidVideoOutputToken, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRelayOutputOptions(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
options, err := client.GetRelayOutputOptions(ctx, "relay_001")
|
||||
if err != nil {
|
||||
t.Fatalf("GetRelayOutputOptions failed: %v", err)
|
||||
}
|
||||
|
||||
if options.Token != "relay_001" {
|
||||
t.Errorf("Expected token 'relay_001', got '%s'", options.Token)
|
||||
}
|
||||
|
||||
if len(options.Mode) != 2 {
|
||||
t.Errorf("Expected 2 modes, got %d", len(options.Mode))
|
||||
}
|
||||
|
||||
if options.Mode[0] != RelayModeMonostable {
|
||||
t.Errorf("Expected first mode to be Monostable, got '%s'", options.Mode[0])
|
||||
}
|
||||
|
||||
if options.Mode[1] != RelayModeBistable {
|
||||
t.Errorf("Expected second mode to be Bistable, got '%s'", options.Mode[1])
|
||||
}
|
||||
|
||||
if len(options.DelayTimes) != 3 {
|
||||
t.Errorf("Expected 3 delay times, got %d", len(options.DelayTimes))
|
||||
}
|
||||
|
||||
if !options.Discrete {
|
||||
t.Error("Expected Discrete to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRelayOutputOptionsInvalidToken(t *testing.T) {
|
||||
server := newMockDeviceIOServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
_, err = client.GetRelayOutputOptions(ctx, "")
|
||||
if !errors.Is(err, ErrInvalidRelayOutputToken) {
|
||||
t.Errorf("Expected ErrInvalidRelayOutputToken, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,778 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/0x524a/onvif-go/internal/soap"
|
||||
)
|
||||
|
||||
// Event service namespace.
|
||||
const eventNamespace = "http://www.onvif.org/ver10/events/wsdl"
|
||||
|
||||
// Event service errors.
|
||||
var (
|
||||
// ErrInvalidSubscriptionReference is returned when subscription reference is invalid.
|
||||
ErrInvalidSubscriptionReference = errors.New("invalid subscription reference")
|
||||
// ErrInvalidTerminationTime is returned when termination time is invalid.
|
||||
ErrInvalidTerminationTime = errors.New("invalid termination time")
|
||||
// ErrInvalidMessageLimit is returned when message limit is invalid.
|
||||
ErrInvalidMessageLimit = errors.New("invalid message limit: must be positive")
|
||||
// ErrInvalidTimeout is returned when timeout is invalid.
|
||||
ErrInvalidTimeout = errors.New("invalid timeout: must be positive")
|
||||
// ErrInvalidFilter is returned when filter expression is invalid.
|
||||
ErrInvalidFilter = errors.New("invalid filter expression")
|
||||
// ErrInvalidEventBrokerAddress is returned when event broker address is empty.
|
||||
ErrInvalidEventBrokerAddress = errors.New("invalid event broker address: cannot be empty")
|
||||
// ErrPullPointNotSupported is returned when pull point is not supported.
|
||||
ErrPullPointNotSupported = errors.New("pull point subscription not supported")
|
||||
// ErrEventBrokerConfigNil is returned when event broker config is nil.
|
||||
ErrEventBrokerConfigNil = errors.New("event broker config cannot be nil")
|
||||
)
|
||||
|
||||
// EventServiceCapabilities represents the capabilities of the event service.
|
||||
type EventServiceCapabilities struct {
|
||||
WSSubscriptionPolicySupport bool
|
||||
WSPausableSubscriptionManagerInterfaceSupport bool
|
||||
MaxNotificationProducers int
|
||||
MaxPullPoints int
|
||||
PersistentNotificationStorage bool
|
||||
EventBrokerProtocols []string
|
||||
MaxEventBrokers int
|
||||
MetadataOverMQTT bool
|
||||
}
|
||||
|
||||
// PullPointSubscription represents a pull point subscription.
|
||||
type PullPointSubscription struct {
|
||||
SubscriptionReference string
|
||||
CurrentTime time.Time
|
||||
TerminationTime time.Time
|
||||
}
|
||||
|
||||
// NotificationMessage represents a notification message from an event.
|
||||
type NotificationMessage struct {
|
||||
Topic string
|
||||
Message EventMessage
|
||||
ProducerAddress string
|
||||
SubscriptionID string
|
||||
}
|
||||
|
||||
// EventMessage represents the content of an event message.
|
||||
type EventMessage struct {
|
||||
PropertyOperation string
|
||||
UtcTime time.Time
|
||||
Source []SimpleItem
|
||||
Key []SimpleItem
|
||||
Data []SimpleItem
|
||||
}
|
||||
|
||||
// EventSimpleItem represents a simple name-value pair in an event message.
|
||||
// Note: Uses SimpleItem from types.go which has the same structure.
|
||||
|
||||
// TopicSet represents the set of topics supported by the device.
|
||||
type TopicSet struct {
|
||||
Topics []Topic
|
||||
}
|
||||
|
||||
// Topic represents an event topic.
|
||||
type Topic struct {
|
||||
Name string
|
||||
Description string
|
||||
Children []Topic
|
||||
}
|
||||
|
||||
// EventBrokerConfig represents an event broker configuration.
|
||||
type EventBrokerConfig struct {
|
||||
Address string
|
||||
TopicPrefix string
|
||||
UserName string
|
||||
Password string
|
||||
CertificateID string
|
||||
PublishFilter string
|
||||
QoS int
|
||||
Status string
|
||||
CertPathValidation bool
|
||||
MetadataFilter string
|
||||
}
|
||||
|
||||
// EventProperties represents the event properties of the device.
|
||||
type EventProperties struct {
|
||||
TopicNamespaceLocation []string
|
||||
FixedTopicSet bool
|
||||
TopicSet TopicSet
|
||||
TopicExpressionDialects []string
|
||||
MessageContentFilterDialects []string
|
||||
ProducerPropertiesFilterDialects []string
|
||||
MessageContentSchemaLocation []string
|
||||
}
|
||||
|
||||
// getEventEndpoint returns the event endpoint, falling back to the default endpoint if not set.
|
||||
func (c *Client) getEventEndpoint() string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if c.eventEndpoint != "" {
|
||||
return c.eventEndpoint
|
||||
}
|
||||
|
||||
return c.endpoint
|
||||
}
|
||||
|
||||
// SetEventEndpoint sets the event service endpoint.
|
||||
func (c *Client) SetEventEndpoint(endpoint string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.eventEndpoint = endpoint
|
||||
}
|
||||
|
||||
// GetEventServiceCapabilities retrieves the capabilities of the event service.
|
||||
func (c *Client) GetEventServiceCapabilities(ctx context.Context) (*EventServiceCapabilities, error) {
|
||||
endpoint := c.getEventEndpoint()
|
||||
|
||||
type GetServiceCapabilities struct {
|
||||
XMLName xml.Name `xml:"tev:GetServiceCapabilities"`
|
||||
Xmlns string `xml:"xmlns:tev,attr"`
|
||||
}
|
||||
|
||||
type GetServiceCapabilitiesResponse struct {
|
||||
XMLName xml.Name `xml:"GetServiceCapabilitiesResponse"`
|
||||
Capabilities struct {
|
||||
WSSubscriptionPolicySupport bool `xml:"WSSubscriptionPolicySupport,attr"`
|
||||
WSPausableSubscriptionManagerInterfaceSupport bool `xml:"WSPausableSubscriptionManagerInterfaceSupport,attr"`
|
||||
MaxNotificationProducers int `xml:"MaxNotificationProducers,attr"`
|
||||
MaxPullPoints int `xml:"MaxPullPoints,attr"`
|
||||
PersistentNotificationStorage bool `xml:"PersistentNotificationStorage,attr"`
|
||||
EventBrokerProtocols string `xml:"EventBrokerProtocols,attr"`
|
||||
MaxEventBrokers int `xml:"MaxEventBrokers,attr"`
|
||||
MetadataOverMQTT bool `xml:"MetadataOverMQTT,attr"`
|
||||
} `xml:"Capabilities"`
|
||||
}
|
||||
|
||||
req := GetServiceCapabilities{
|
||||
Xmlns: eventNamespace,
|
||||
}
|
||||
|
||||
var resp GetServiceCapabilitiesResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("GetEventServiceCapabilities failed: %w", err)
|
||||
}
|
||||
|
||||
caps := &EventServiceCapabilities{
|
||||
WSSubscriptionPolicySupport: resp.Capabilities.WSSubscriptionPolicySupport,
|
||||
WSPausableSubscriptionManagerInterfaceSupport: resp.Capabilities.WSPausableSubscriptionManagerInterfaceSupport,
|
||||
MaxNotificationProducers: resp.Capabilities.MaxNotificationProducers,
|
||||
MaxPullPoints: resp.Capabilities.MaxPullPoints,
|
||||
PersistentNotificationStorage: resp.Capabilities.PersistentNotificationStorage,
|
||||
MaxEventBrokers: resp.Capabilities.MaxEventBrokers,
|
||||
MetadataOverMQTT: resp.Capabilities.MetadataOverMQTT,
|
||||
}
|
||||
|
||||
// Parse event broker protocols from space-separated string.
|
||||
if resp.Capabilities.EventBrokerProtocols != "" {
|
||||
caps.EventBrokerProtocols = splitSpaceSeparated(resp.Capabilities.EventBrokerProtocols)
|
||||
}
|
||||
|
||||
return caps, nil
|
||||
}
|
||||
|
||||
// CreatePullPointSubscription creates a new pull point subscription.
|
||||
func (c *Client) CreatePullPointSubscription(
|
||||
ctx context.Context,
|
||||
filter string,
|
||||
initialTerminationTime *time.Duration,
|
||||
subscriptionPolicy string,
|
||||
) (*PullPointSubscription, error) {
|
||||
endpoint := c.getEventEndpoint()
|
||||
|
||||
type Filter struct {
|
||||
TopicExpression string `xml:"wsnt:TopicExpression,omitempty"`
|
||||
}
|
||||
|
||||
type CreatePullPointSubscription struct {
|
||||
XMLName xml.Name `xml:"tev:CreatePullPointSubscription"`
|
||||
XmlnsTev string `xml:"xmlns:tev,attr"`
|
||||
XmlnsWsnt string `xml:"xmlns:wsnt,attr"`
|
||||
Filter *Filter `xml:"tev:Filter,omitempty"`
|
||||
InitialTerminationTime string `xml:"tev:InitialTerminationTime,omitempty"`
|
||||
SubscriptionPolicy string `xml:"tev:SubscriptionPolicy,omitempty"`
|
||||
}
|
||||
|
||||
type CreatePullPointSubscriptionResponse struct {
|
||||
XMLName xml.Name `xml:"CreatePullPointSubscriptionResponse"`
|
||||
SubscriptionReference struct {
|
||||
Address string `xml:"Address"`
|
||||
} `xml:"SubscriptionReference"`
|
||||
CurrentTime string `xml:"CurrentTime"`
|
||||
TerminationTime string `xml:"TerminationTime"`
|
||||
}
|
||||
|
||||
req := CreatePullPointSubscription{
|
||||
XmlnsTev: eventNamespace,
|
||||
XmlnsWsnt: "http://docs.oasis-open.org/wsn/b-2",
|
||||
}
|
||||
|
||||
if filter != "" {
|
||||
req.Filter = &Filter{
|
||||
TopicExpression: filter,
|
||||
}
|
||||
}
|
||||
|
||||
if initialTerminationTime != nil {
|
||||
if *initialTerminationTime <= 0 {
|
||||
return nil, ErrInvalidTerminationTime
|
||||
}
|
||||
req.InitialTerminationTime = formatDuration(*initialTerminationTime)
|
||||
}
|
||||
|
||||
if subscriptionPolicy != "" {
|
||||
req.SubscriptionPolicy = subscriptionPolicy
|
||||
}
|
||||
|
||||
var resp CreatePullPointSubscriptionResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("CreatePullPointSubscription failed: %w", err)
|
||||
}
|
||||
|
||||
subscription := &PullPointSubscription{
|
||||
SubscriptionReference: resp.SubscriptionReference.Address,
|
||||
}
|
||||
|
||||
if resp.CurrentTime != "" {
|
||||
if t, err := time.Parse(time.RFC3339, resp.CurrentTime); err == nil {
|
||||
subscription.CurrentTime = t
|
||||
}
|
||||
}
|
||||
|
||||
if resp.TerminationTime != "" {
|
||||
if t, err := time.Parse(time.RFC3339, resp.TerminationTime); err == nil {
|
||||
subscription.TerminationTime = t
|
||||
}
|
||||
}
|
||||
|
||||
return subscription, nil
|
||||
}
|
||||
|
||||
// PullMessages pulls notification messages from a pull point subscription.
|
||||
func (c *Client) PullMessages(
|
||||
ctx context.Context,
|
||||
subscriptionReference string,
|
||||
timeout time.Duration,
|
||||
messageLimit int,
|
||||
) ([]NotificationMessage, error) {
|
||||
if subscriptionReference == "" {
|
||||
return nil, ErrInvalidSubscriptionReference
|
||||
}
|
||||
|
||||
if timeout <= 0 {
|
||||
return nil, ErrInvalidTimeout
|
||||
}
|
||||
|
||||
if messageLimit <= 0 {
|
||||
return nil, ErrInvalidMessageLimit
|
||||
}
|
||||
|
||||
type PullMessages struct {
|
||||
XMLName xml.Name `xml:"tev:PullMessages"`
|
||||
Xmlns string `xml:"xmlns:tev,attr"`
|
||||
Timeout string `xml:"tev:Timeout"`
|
||||
MessageLimit int `xml:"tev:MessageLimit"`
|
||||
}
|
||||
|
||||
type SimpleItemXML struct {
|
||||
Name string `xml:"Name,attr"`
|
||||
Value string `xml:"Value,attr"`
|
||||
}
|
||||
|
||||
type PullMessagesResponse struct {
|
||||
XMLName xml.Name `xml:"PullMessagesResponse"`
|
||||
CurrentTime string `xml:"CurrentTime"`
|
||||
TerminationTime string `xml:"TerminationTime"`
|
||||
NotificationMessages []struct {
|
||||
Topic struct {
|
||||
Value string `xml:",chardata"`
|
||||
} `xml:"Topic"`
|
||||
ProducerReference struct {
|
||||
Address string `xml:"Address"`
|
||||
} `xml:"ProducerReference"`
|
||||
Message struct {
|
||||
PropertyOperation string `xml:"PropertyOperation,attr"`
|
||||
UtcTime string `xml:"UtcTime,attr"`
|
||||
Source struct {
|
||||
SimpleItems []SimpleItemXML `xml:"SimpleItem"`
|
||||
} `xml:"Source"`
|
||||
Key struct {
|
||||
SimpleItems []SimpleItemXML `xml:"SimpleItem"`
|
||||
} `xml:"Key"`
|
||||
Data struct {
|
||||
SimpleItems []SimpleItemXML `xml:"SimpleItem"`
|
||||
} `xml:"Data"`
|
||||
} `xml:"Message"`
|
||||
} `xml:"NotificationMessage"`
|
||||
}
|
||||
|
||||
req := PullMessages{
|
||||
Xmlns: eventNamespace,
|
||||
Timeout: formatDuration(timeout),
|
||||
MessageLimit: messageLimit,
|
||||
}
|
||||
|
||||
var resp PullMessagesResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, subscriptionReference, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("PullMessages failed: %w", err)
|
||||
}
|
||||
|
||||
messages := make([]NotificationMessage, len(resp.NotificationMessages))
|
||||
for i := range resp.NotificationMessages {
|
||||
nm := &resp.NotificationMessages[i]
|
||||
msg := NotificationMessage{
|
||||
Topic: nm.Topic.Value,
|
||||
ProducerAddress: nm.ProducerReference.Address,
|
||||
}
|
||||
|
||||
msg.Message.PropertyOperation = nm.Message.PropertyOperation
|
||||
|
||||
if nm.Message.UtcTime != "" {
|
||||
if t, err := time.Parse(time.RFC3339, nm.Message.UtcTime); err == nil {
|
||||
msg.Message.UtcTime = t
|
||||
}
|
||||
}
|
||||
|
||||
// Convert source items.
|
||||
msg.Message.Source = make([]SimpleItem, len(nm.Message.Source.SimpleItems))
|
||||
for j, item := range nm.Message.Source.SimpleItems {
|
||||
msg.Message.Source[j] = SimpleItem{Name: item.Name, Value: item.Value}
|
||||
}
|
||||
|
||||
// Convert key items.
|
||||
msg.Message.Key = make([]SimpleItem, len(nm.Message.Key.SimpleItems))
|
||||
for j, item := range nm.Message.Key.SimpleItems {
|
||||
msg.Message.Key[j] = SimpleItem{Name: item.Name, Value: item.Value}
|
||||
}
|
||||
|
||||
// Convert data items.
|
||||
msg.Message.Data = make([]SimpleItem, len(nm.Message.Data.SimpleItems))
|
||||
for j, item := range nm.Message.Data.SimpleItems {
|
||||
msg.Message.Data[j] = SimpleItem{Name: item.Name, Value: item.Value}
|
||||
}
|
||||
|
||||
messages[i] = msg
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
// Seek seeks to a specific position in the event stream.
|
||||
func (c *Client) Seek(ctx context.Context, subscriptionReference string, utcTime time.Time, reverse bool) error {
|
||||
if subscriptionReference == "" {
|
||||
return ErrInvalidSubscriptionReference
|
||||
}
|
||||
|
||||
type Seek struct {
|
||||
XMLName xml.Name `xml:"tev:Seek"`
|
||||
Xmlns string `xml:"xmlns:tev,attr"`
|
||||
UtcTime string `xml:"tev:UtcTime"`
|
||||
Reverse bool `xml:"tev:Reverse,omitempty"`
|
||||
}
|
||||
|
||||
type SeekResponse struct {
|
||||
XMLName xml.Name `xml:"SeekResponse"`
|
||||
}
|
||||
|
||||
req := Seek{
|
||||
Xmlns: eventNamespace,
|
||||
UtcTime: utcTime.Format(time.RFC3339),
|
||||
Reverse: reverse,
|
||||
}
|
||||
|
||||
var resp SeekResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, subscriptionReference, "", req, &resp); err != nil {
|
||||
return fmt.Errorf("Seek failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetEventSynchronizationPoint instructs the device to send a synchronization point for events.
|
||||
func (c *Client) SetEventSynchronizationPoint(ctx context.Context, subscriptionReference string) error {
|
||||
if subscriptionReference == "" {
|
||||
return ErrInvalidSubscriptionReference
|
||||
}
|
||||
|
||||
type SetSynchronizationPoint struct {
|
||||
XMLName xml.Name `xml:"tev:SetSynchronizationPoint"`
|
||||
Xmlns string `xml:"xmlns:tev,attr"`
|
||||
}
|
||||
|
||||
type SetSynchronizationPointResponse struct {
|
||||
XMLName xml.Name `xml:"SetSynchronizationPointResponse"`
|
||||
}
|
||||
|
||||
req := SetSynchronizationPoint{
|
||||
Xmlns: eventNamespace,
|
||||
}
|
||||
|
||||
var resp SetSynchronizationPointResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, subscriptionReference, "", req, &resp); err != nil {
|
||||
return fmt.Errorf("SetSynchronizationPoint failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unsubscribe terminates a subscription.
|
||||
func (c *Client) Unsubscribe(ctx context.Context, subscriptionReference string) error {
|
||||
if subscriptionReference == "" {
|
||||
return ErrInvalidSubscriptionReference
|
||||
}
|
||||
|
||||
type Unsubscribe struct {
|
||||
XMLName xml.Name `xml:"wsnt:Unsubscribe"`
|
||||
Xmlns string `xml:"xmlns:wsnt,attr"`
|
||||
}
|
||||
|
||||
type UnsubscribeResponse struct {
|
||||
XMLName xml.Name `xml:"UnsubscribeResponse"`
|
||||
}
|
||||
|
||||
req := Unsubscribe{
|
||||
Xmlns: "http://docs.oasis-open.org/wsn/b-2",
|
||||
}
|
||||
|
||||
var resp UnsubscribeResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, subscriptionReference, "", req, &resp); err != nil {
|
||||
return fmt.Errorf("Unsubscribe failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenewSubscription renews a subscription with a new termination time.
|
||||
func (c *Client) RenewSubscription(
|
||||
ctx context.Context,
|
||||
subscriptionReference string,
|
||||
terminationTime time.Duration,
|
||||
) (time.Time, time.Time, error) {
|
||||
if subscriptionReference == "" {
|
||||
return time.Time{}, time.Time{}, ErrInvalidSubscriptionReference
|
||||
}
|
||||
|
||||
if terminationTime <= 0 {
|
||||
return time.Time{}, time.Time{}, ErrInvalidTerminationTime
|
||||
}
|
||||
|
||||
type Renew struct {
|
||||
XMLName xml.Name `xml:"wsnt:Renew"`
|
||||
Xmlns string `xml:"xmlns:wsnt,attr"`
|
||||
TerminationTime string `xml:"wsnt:TerminationTime"`
|
||||
}
|
||||
|
||||
type RenewResponse struct {
|
||||
XMLName xml.Name `xml:"RenewResponse"`
|
||||
CurrentTime string `xml:"CurrentTime"`
|
||||
TerminationTime string `xml:"TerminationTime"`
|
||||
}
|
||||
|
||||
req := Renew{
|
||||
Xmlns: "http://docs.oasis-open.org/wsn/b-2",
|
||||
TerminationTime: formatDuration(terminationTime),
|
||||
}
|
||||
|
||||
var resp RenewResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, subscriptionReference, "", req, &resp); err != nil {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("RenewSubscription failed: %w", err)
|
||||
}
|
||||
|
||||
var currentTime, newTerminationTime time.Time
|
||||
|
||||
if resp.CurrentTime != "" {
|
||||
if t, err := time.Parse(time.RFC3339, resp.CurrentTime); err == nil {
|
||||
currentTime = t
|
||||
}
|
||||
}
|
||||
|
||||
if resp.TerminationTime != "" {
|
||||
if t, err := time.Parse(time.RFC3339, resp.TerminationTime); err == nil {
|
||||
newTerminationTime = t
|
||||
}
|
||||
}
|
||||
|
||||
return currentTime, newTerminationTime, nil
|
||||
}
|
||||
|
||||
// GetEventProperties retrieves the event properties of the device.
|
||||
func (c *Client) GetEventProperties(ctx context.Context) (*EventProperties, error) {
|
||||
endpoint := c.getEventEndpoint()
|
||||
|
||||
type GetEventProperties struct {
|
||||
XMLName xml.Name `xml:"tev:GetEventProperties"`
|
||||
Xmlns string `xml:"xmlns:tev,attr"`
|
||||
}
|
||||
|
||||
type GetEventPropertiesResponse struct {
|
||||
XMLName xml.Name `xml:"GetEventPropertiesResponse"`
|
||||
TopicNamespaceLocation []string `xml:"TopicNamespaceLocation"`
|
||||
FixedTopicSet bool `xml:"FixedTopicSet"`
|
||||
TopicExpressionDialect []string `xml:"TopicExpressionDialect"`
|
||||
MessageContentFilterDialect []string `xml:"MessageContentFilterDialect"`
|
||||
ProducerPropertiesFilterDialect []string `xml:"ProducerPropertiesFilterDialect"`
|
||||
MessageContentSchemaLocation []string `xml:"MessageContentSchemaLocation"`
|
||||
}
|
||||
|
||||
req := GetEventProperties{
|
||||
Xmlns: eventNamespace,
|
||||
}
|
||||
|
||||
var resp GetEventPropertiesResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("GetEventProperties failed: %w", err)
|
||||
}
|
||||
|
||||
properties := &EventProperties{
|
||||
TopicNamespaceLocation: resp.TopicNamespaceLocation,
|
||||
FixedTopicSet: resp.FixedTopicSet,
|
||||
TopicExpressionDialects: resp.TopicExpressionDialect,
|
||||
MessageContentFilterDialects: resp.MessageContentFilterDialect,
|
||||
ProducerPropertiesFilterDialects: resp.ProducerPropertiesFilterDialect,
|
||||
MessageContentSchemaLocation: resp.MessageContentSchemaLocation,
|
||||
}
|
||||
|
||||
return properties, nil
|
||||
}
|
||||
|
||||
// AddEventBroker adds an event broker configuration.
|
||||
func (c *Client) AddEventBroker(ctx context.Context, config *EventBrokerConfig) error {
|
||||
if config == nil {
|
||||
return ErrEventBrokerConfigNil
|
||||
}
|
||||
|
||||
if config.Address == "" {
|
||||
return ErrInvalidEventBrokerAddress
|
||||
}
|
||||
|
||||
endpoint := c.getEventEndpoint()
|
||||
|
||||
type EventBrokerConfigXML struct {
|
||||
Address string `xml:"tev:Address"`
|
||||
TopicPrefix string `xml:"tev:TopicPrefix,omitempty"`
|
||||
UserName string `xml:"tev:UserName,omitempty"`
|
||||
Password string `xml:"tev:Password,omitempty"`
|
||||
CertificateID string `xml:"tev:CertificateID,omitempty"`
|
||||
PublishFilter string `xml:"tev:PublishFilter,omitempty"`
|
||||
QoS int `xml:"tev:QoS,omitempty"`
|
||||
CertPathValidation bool `xml:"tev:CertPathValidation,omitempty"`
|
||||
MetadataFilter string `xml:"tev:MetadataFilter,omitempty"`
|
||||
}
|
||||
|
||||
type AddEventBroker struct {
|
||||
XMLName xml.Name `xml:"tev:AddEventBroker"`
|
||||
Xmlns string `xml:"xmlns:tev,attr"`
|
||||
EventBrokerConfig EventBrokerConfigXML `xml:"tev:EventBrokerConfig"`
|
||||
}
|
||||
|
||||
type AddEventBrokerResponse struct {
|
||||
XMLName xml.Name `xml:"AddEventBrokerResponse"`
|
||||
}
|
||||
|
||||
req := AddEventBroker{
|
||||
Xmlns: eventNamespace,
|
||||
EventBrokerConfig: EventBrokerConfigXML{
|
||||
Address: config.Address,
|
||||
TopicPrefix: config.TopicPrefix,
|
||||
UserName: config.UserName,
|
||||
Password: config.Password,
|
||||
CertificateID: config.CertificateID,
|
||||
PublishFilter: config.PublishFilter,
|
||||
QoS: config.QoS,
|
||||
CertPathValidation: config.CertPathValidation,
|
||||
MetadataFilter: config.MetadataFilter,
|
||||
},
|
||||
}
|
||||
|
||||
var resp AddEventBrokerResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return fmt.Errorf("AddEventBroker failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteEventBroker deletes an event broker configuration.
|
||||
func (c *Client) DeleteEventBroker(ctx context.Context, address string) error {
|
||||
if address == "" {
|
||||
return ErrInvalidEventBrokerAddress
|
||||
}
|
||||
|
||||
endpoint := c.getEventEndpoint()
|
||||
|
||||
type DeleteEventBroker struct {
|
||||
XMLName xml.Name `xml:"tev:DeleteEventBroker"`
|
||||
Xmlns string `xml:"xmlns:tev,attr"`
|
||||
Address string `xml:"tev:Address"`
|
||||
}
|
||||
|
||||
type DeleteEventBrokerResponse struct {
|
||||
XMLName xml.Name `xml:"DeleteEventBrokerResponse"`
|
||||
}
|
||||
|
||||
req := DeleteEventBroker{
|
||||
Xmlns: eventNamespace,
|
||||
Address: address,
|
||||
}
|
||||
|
||||
var resp DeleteEventBrokerResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return fmt.Errorf("DeleteEventBroker failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEventBrokers retrieves all event broker configurations.
|
||||
func (c *Client) GetEventBrokers(ctx context.Context) ([]*EventBrokerConfig, error) {
|
||||
endpoint := c.getEventEndpoint()
|
||||
|
||||
type GetEventBrokers struct {
|
||||
XMLName xml.Name `xml:"tev:GetEventBrokers"`
|
||||
Xmlns string `xml:"xmlns:tev,attr"`
|
||||
}
|
||||
|
||||
type GetEventBrokersResponse struct {
|
||||
XMLName xml.Name `xml:"GetEventBrokersResponse"`
|
||||
EventBrokers []struct {
|
||||
Address string `xml:"Address"`
|
||||
TopicPrefix string `xml:"TopicPrefix"`
|
||||
UserName string `xml:"UserName"`
|
||||
Password string `xml:"Password"`
|
||||
CertificateID string `xml:"CertificateID"`
|
||||
PublishFilter string `xml:"PublishFilter"`
|
||||
QoS int `xml:"QoS"`
|
||||
Status string `xml:"Status"`
|
||||
CertPathValidation bool `xml:"CertPathValidation"`
|
||||
MetadataFilter string `xml:"MetadataFilter"`
|
||||
} `xml:"EventBroker"`
|
||||
}
|
||||
|
||||
req := GetEventBrokers{
|
||||
Xmlns: eventNamespace,
|
||||
}
|
||||
|
||||
var resp GetEventBrokersResponse
|
||||
|
||||
username, password := c.GetCredentials()
|
||||
soapClient := soap.NewClient(c.httpClient, username, password)
|
||||
|
||||
if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil {
|
||||
return nil, fmt.Errorf("GetEventBrokers failed: %w", err)
|
||||
}
|
||||
|
||||
brokers := make([]*EventBrokerConfig, len(resp.EventBrokers))
|
||||
for i := range resp.EventBrokers {
|
||||
eb := &resp.EventBrokers[i]
|
||||
brokers[i] = &EventBrokerConfig{
|
||||
Address: eb.Address,
|
||||
TopicPrefix: eb.TopicPrefix,
|
||||
UserName: eb.UserName,
|
||||
Password: eb.Password,
|
||||
CertificateID: eb.CertificateID,
|
||||
PublishFilter: eb.PublishFilter,
|
||||
QoS: eb.QoS,
|
||||
Status: eb.Status,
|
||||
CertPathValidation: eb.CertPathValidation,
|
||||
MetadataFilter: eb.MetadataFilter,
|
||||
}
|
||||
}
|
||||
|
||||
return brokers, nil
|
||||
}
|
||||
|
||||
// formatDuration formats a duration as an ISO 8601 duration string.
|
||||
func formatDuration(d time.Duration) string {
|
||||
seconds := int(d.Seconds())
|
||||
if seconds < 60 { //nolint:mnd // 60 seconds in a minute
|
||||
return fmt.Sprintf("PT%dS", seconds)
|
||||
}
|
||||
|
||||
minutes := seconds / 60 //nolint:mnd // 60 seconds in a minute
|
||||
seconds %= 60
|
||||
|
||||
if seconds == 0 {
|
||||
return fmt.Sprintf("PT%dM", minutes)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("PT%dM%dS", minutes, seconds)
|
||||
}
|
||||
|
||||
// splitSpaceSeparated splits a space-separated string into a slice.
|
||||
func splitSpaceSeparated(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result []string
|
||||
|
||||
start := 0
|
||||
inWord := false
|
||||
|
||||
for i, r := range s {
|
||||
if r == ' ' || r == '\t' {
|
||||
if inWord {
|
||||
result = append(result, s[start:i])
|
||||
inWord = false
|
||||
}
|
||||
} else {
|
||||
if !inWord {
|
||||
start = i
|
||||
inWord = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if inWord {
|
||||
result = append(result, s[start:])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
+738
@@ -0,0 +1,738 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const testEventXMLHeader = `<?xml version="1.0" encoding="UTF-8"?>`
|
||||
|
||||
func newMockEventServer() *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/soap+xml")
|
||||
|
||||
body := make([]byte, r.ContentLength)
|
||||
_, _ = r.Body.Read(body)
|
||||
bodyStr := string(body)
|
||||
|
||||
var response string
|
||||
|
||||
switch {
|
||||
case strings.Contains(bodyStr, "GetServiceCapabilities"):
|
||||
response = testEventXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tev:GetServiceCapabilitiesResponse xmlns:tev="http://www.onvif.org/ver10/events/wsdl">
|
||||
<tev:Capabilities
|
||||
WSSubscriptionPolicySupport="true"
|
||||
WSPausableSubscriptionManagerInterfaceSupport="true"
|
||||
MaxNotificationProducers="10"
|
||||
MaxPullPoints="5"
|
||||
PersistentNotificationStorage="true"
|
||||
EventBrokerProtocols="mqtt mqtts"
|
||||
MaxEventBrokers="3"
|
||||
MetadataOverMQTT="true"/>
|
||||
</tev:GetServiceCapabilitiesResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "CreatePullPointSubscription"):
|
||||
response = testEventXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tev:CreatePullPointSubscriptionResponse xmlns:tev="http://www.onvif.org/ver10/events/wsdl">
|
||||
<tev:SubscriptionReference>
|
||||
<wsa:Address xmlns:wsa="http://www.w3.org/2005/08/addressing">http://192.168.1.100/onvif/subscription/1</wsa:Address>
|
||||
</tev:SubscriptionReference>
|
||||
<tev:CurrentTime>2025-01-15T10:30:00Z</tev:CurrentTime>
|
||||
<tev:TerminationTime>2025-01-15T11:30:00Z</tev:TerminationTime>
|
||||
</tev:CreatePullPointSubscriptionResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "PullMessages"):
|
||||
response = testEventXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tev:PullMessagesResponse xmlns:tev="http://www.onvif.org/ver10/events/wsdl">
|
||||
<tev:CurrentTime>2025-01-15T10:30:00Z</tev:CurrentTime>
|
||||
<tev:TerminationTime>2025-01-15T11:30:00Z</tev:TerminationTime>
|
||||
<wsnt:NotificationMessage xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2">
|
||||
<wsnt:Topic>tns1:VideoSource/MotionAlarm</wsnt:Topic>
|
||||
<wsnt:ProducerReference>
|
||||
<wsa:Address xmlns:wsa="http://www.w3.org/2005/08/addressing">http://192.168.1.100</wsa:Address>
|
||||
</wsnt:ProducerReference>
|
||||
<wsnt:Message PropertyOperation="Changed" UtcTime="2025-01-15T10:29:55Z">
|
||||
<tt:Source xmlns:tt="http://www.onvif.org/ver10/schema">
|
||||
<tt:SimpleItem Name="VideoSourceToken" Value="video_src_001"/>
|
||||
</tt:Source>
|
||||
<tt:Key xmlns:tt="http://www.onvif.org/ver10/schema">
|
||||
<tt:SimpleItem Name="RuleToken" Value="rule_001"/>
|
||||
</tt:Key>
|
||||
<tt:Data xmlns:tt="http://www.onvif.org/ver10/schema">
|
||||
<tt:SimpleItem Name="State" Value="true"/>
|
||||
</tt:Data>
|
||||
</wsnt:Message>
|
||||
</wsnt:NotificationMessage>
|
||||
</tev:PullMessagesResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "Seek"):
|
||||
response = testEventXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tev:SeekResponse xmlns:tev="http://www.onvif.org/ver10/events/wsdl"/>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "SetSynchronizationPoint"):
|
||||
response = testEventXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tev:SetSynchronizationPointResponse xmlns:tev="http://www.onvif.org/ver10/events/wsdl"/>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "Unsubscribe"):
|
||||
response = testEventXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<wsnt:UnsubscribeResponse xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2"/>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "Renew"):
|
||||
response = testEventXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<wsnt:RenewResponse xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2">
|
||||
<wsnt:CurrentTime>2025-01-15T10:30:00Z</wsnt:CurrentTime>
|
||||
<wsnt:TerminationTime>2025-01-15T12:30:00Z</wsnt:TerminationTime>
|
||||
</wsnt:RenewResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "GetEventProperties"):
|
||||
response = testEventXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tev:GetEventPropertiesResponse xmlns:tev="http://www.onvif.org/ver10/events/wsdl">
|
||||
<tev:TopicNamespaceLocation>http://www.onvif.org/onvif/ver10/topics/topicns.xml</tev:TopicNamespaceLocation>
|
||||
<tev:FixedTopicSet>true</tev:FixedTopicSet>
|
||||
<tev:TopicExpressionDialect>http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet</tev:TopicExpressionDialect>
|
||||
<tev:MessageContentFilterDialect>http://www.onvif.org/ver10/tev/messageContentFilter/ItemFilter</tev:MessageContentFilterDialect>
|
||||
<tev:ProducerPropertiesFilterDialect>http://www.onvif.org/ver10/tev/producerPropertiesFilter</tev:ProducerPropertiesFilterDialect>
|
||||
<tev:MessageContentSchemaLocation>http://www.onvif.org/onvif/ver10/schema/onvif.xsd</tev:MessageContentSchemaLocation>
|
||||
</tev:GetEventPropertiesResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "AddEventBroker"):
|
||||
response = testEventXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tev:AddEventBrokerResponse xmlns:tev="http://www.onvif.org/ver10/events/wsdl"/>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "DeleteEventBroker"):
|
||||
response = testEventXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tev:DeleteEventBrokerResponse xmlns:tev="http://www.onvif.org/ver10/events/wsdl"/>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
case strings.Contains(bodyStr, "GetEventBrokers"):
|
||||
response = testEventXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tev:GetEventBrokersResponse xmlns:tev="http://www.onvif.org/ver10/events/wsdl">
|
||||
<tev:EventBroker>
|
||||
<tev:Address>mqtt://broker.example.com:1883</tev:Address>
|
||||
<tev:TopicPrefix>onvif/</tev:TopicPrefix>
|
||||
<tev:UserName>mqtt_user</tev:UserName>
|
||||
<tev:QoS>1</tev:QoS>
|
||||
<tev:Status>Connected</tev:Status>
|
||||
<tev:CertPathValidation>true</tev:CertPathValidation>
|
||||
</tev:EventBroker>
|
||||
</tev:GetEventBrokersResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
default:
|
||||
response = testEventXMLHeader + `
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<SOAP-ENV:Fault>
|
||||
<SOAP-ENV:Code><SOAP-ENV:Value>SOAP-ENV:Receiver</SOAP-ENV:Value></SOAP-ENV:Code>
|
||||
<SOAP-ENV:Reason><SOAP-ENV:Text>Unknown action</SOAP-ENV:Text></SOAP-ENV:Reason>
|
||||
</SOAP-ENV:Fault>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte(response))
|
||||
}))
|
||||
}
|
||||
|
||||
func TestGetEventServiceCapabilities(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
caps, err := client.GetEventServiceCapabilities(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetEventServiceCapabilities failed: %v", err)
|
||||
}
|
||||
|
||||
if !caps.WSSubscriptionPolicySupport {
|
||||
t.Error("Expected WSSubscriptionPolicySupport to be true")
|
||||
}
|
||||
|
||||
if !caps.WSPausableSubscriptionManagerInterfaceSupport {
|
||||
t.Error("Expected WSPausableSubscriptionManagerInterfaceSupport to be true")
|
||||
}
|
||||
|
||||
if caps.MaxNotificationProducers != 10 {
|
||||
t.Errorf("Expected MaxNotificationProducers to be 10, got %d", caps.MaxNotificationProducers)
|
||||
}
|
||||
|
||||
if caps.MaxPullPoints != 5 {
|
||||
t.Errorf("Expected MaxPullPoints to be 5, got %d", caps.MaxPullPoints)
|
||||
}
|
||||
|
||||
if !caps.PersistentNotificationStorage {
|
||||
t.Error("Expected PersistentNotificationStorage to be true")
|
||||
}
|
||||
|
||||
if len(caps.EventBrokerProtocols) != 2 {
|
||||
t.Errorf("Expected 2 EventBrokerProtocols, got %d", len(caps.EventBrokerProtocols))
|
||||
}
|
||||
|
||||
if caps.MaxEventBrokers != 3 {
|
||||
t.Errorf("Expected MaxEventBrokers to be 3, got %d", caps.MaxEventBrokers)
|
||||
}
|
||||
|
||||
if !caps.MetadataOverMQTT {
|
||||
t.Error("Expected MetadataOverMQTT to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePullPointSubscription(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with no filter and default termination time.
|
||||
sub, err := client.CreatePullPointSubscription(ctx, "", nil, "")
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePullPointSubscription failed: %v", err)
|
||||
}
|
||||
|
||||
if sub.SubscriptionReference == "" {
|
||||
t.Error("Expected SubscriptionReference to be set")
|
||||
}
|
||||
|
||||
if sub.CurrentTime.IsZero() {
|
||||
t.Error("Expected CurrentTime to be set")
|
||||
}
|
||||
|
||||
if sub.TerminationTime.IsZero() {
|
||||
t.Error("Expected TerminationTime to be set")
|
||||
}
|
||||
|
||||
// Test with filter and termination time.
|
||||
termTime := 1 * time.Hour
|
||||
sub2, err := client.CreatePullPointSubscription(ctx, "tns1:VideoSource/MotionAlarm", &termTime, "policy1")
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePullPointSubscription with filter failed: %v", err)
|
||||
}
|
||||
|
||||
if sub2.SubscriptionReference == "" {
|
||||
t.Error("Expected SubscriptionReference to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePullPointSubscriptionInvalidTerminationTime(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with invalid (negative) termination time.
|
||||
invalidTime := -1 * time.Hour
|
||||
_, err = client.CreatePullPointSubscription(ctx, "", &invalidTime, "")
|
||||
if !errors.Is(err, ErrInvalidTerminationTime) {
|
||||
t.Errorf("Expected ErrInvalidTerminationTime, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullMessages(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
messages, err := client.PullMessages(ctx, server.URL+"/subscription/1", 30*time.Second, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("PullMessages failed: %v", err)
|
||||
}
|
||||
|
||||
if len(messages) == 0 {
|
||||
t.Error("Expected at least one notification message")
|
||||
}
|
||||
|
||||
if len(messages) > 0 {
|
||||
msg := messages[0]
|
||||
if msg.Topic == "" {
|
||||
t.Error("Expected Topic to be set")
|
||||
}
|
||||
|
||||
if msg.Message.PropertyOperation == "" {
|
||||
t.Error("Expected PropertyOperation to be set")
|
||||
}
|
||||
|
||||
if len(msg.Message.Source) == 0 {
|
||||
t.Error("Expected Source items to be present")
|
||||
}
|
||||
|
||||
if len(msg.Message.Data) == 0 {
|
||||
t.Error("Expected Data items to be present")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullMessagesValidation(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test empty subscription reference.
|
||||
_, err = client.PullMessages(ctx, "", 30*time.Second, 10)
|
||||
if !errors.Is(err, ErrInvalidSubscriptionReference) {
|
||||
t.Errorf("Expected ErrInvalidSubscriptionReference, got %v", err)
|
||||
}
|
||||
|
||||
// Test invalid timeout.
|
||||
_, err = client.PullMessages(ctx, server.URL+"/subscription/1", 0, 10)
|
||||
if !errors.Is(err, ErrInvalidTimeout) {
|
||||
t.Errorf("Expected ErrInvalidTimeout, got %v", err)
|
||||
}
|
||||
|
||||
// Test invalid message limit.
|
||||
_, err = client.PullMessages(ctx, server.URL+"/subscription/1", 30*time.Second, 0)
|
||||
if !errors.Is(err, ErrInvalidMessageLimit) {
|
||||
t.Errorf("Expected ErrInvalidMessageLimit, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeek(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err = client.Seek(ctx, server.URL+"/subscription/1", time.Now().Add(-1*time.Hour), false)
|
||||
if err != nil {
|
||||
t.Fatalf("Seek failed: %v", err)
|
||||
}
|
||||
|
||||
// Test with reverse.
|
||||
err = client.Seek(ctx, server.URL+"/subscription/1", time.Now().Add(-1*time.Hour), true)
|
||||
if err != nil {
|
||||
t.Fatalf("Seek with reverse failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeekInvalidSubscriptionReference(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err = client.Seek(ctx, "", time.Now(), false)
|
||||
if !errors.Is(err, ErrInvalidSubscriptionReference) {
|
||||
t.Errorf("Expected ErrInvalidSubscriptionReference, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetEventSynchronizationPoint(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err = client.SetEventSynchronizationPoint(ctx, server.URL+"/subscription/1")
|
||||
if err != nil {
|
||||
t.Fatalf("SetEventSynchronizationPoint failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetEventSynchronizationPointInvalidSubscriptionReference(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err = client.SetEventSynchronizationPoint(ctx, "")
|
||||
if !errors.Is(err, ErrInvalidSubscriptionReference) {
|
||||
t.Errorf("Expected ErrInvalidSubscriptionReference, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsubscribe(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err = client.Unsubscribe(ctx, server.URL+"/subscription/1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unsubscribe failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsubscribeInvalidSubscriptionReference(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err = client.Unsubscribe(ctx, "")
|
||||
if !errors.Is(err, ErrInvalidSubscriptionReference) {
|
||||
t.Errorf("Expected ErrInvalidSubscriptionReference, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewSubscription(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
currentTime, terminationTime, err := client.RenewSubscription(ctx, server.URL+"/subscription/1", 2*time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("RenewSubscription failed: %v", err)
|
||||
}
|
||||
|
||||
if currentTime.IsZero() {
|
||||
t.Error("Expected CurrentTime to be set")
|
||||
}
|
||||
|
||||
if terminationTime.IsZero() {
|
||||
t.Error("Expected TerminationTime to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewSubscriptionValidation(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test empty subscription reference.
|
||||
_, _, err = client.RenewSubscription(ctx, "", time.Hour)
|
||||
if !errors.Is(err, ErrInvalidSubscriptionReference) {
|
||||
t.Errorf("Expected ErrInvalidSubscriptionReference, got %v", err)
|
||||
}
|
||||
|
||||
// Test invalid termination time.
|
||||
_, _, err = client.RenewSubscription(ctx, server.URL+"/subscription/1", 0)
|
||||
if !errors.Is(err, ErrInvalidTerminationTime) {
|
||||
t.Errorf("Expected ErrInvalidTerminationTime, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEventProperties(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
props, err := client.GetEventProperties(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetEventProperties failed: %v", err)
|
||||
}
|
||||
|
||||
if len(props.TopicNamespaceLocation) == 0 {
|
||||
t.Error("Expected TopicNamespaceLocation to be set")
|
||||
}
|
||||
|
||||
if !props.FixedTopicSet {
|
||||
t.Error("Expected FixedTopicSet to be true")
|
||||
}
|
||||
|
||||
if len(props.TopicExpressionDialects) == 0 {
|
||||
t.Error("Expected TopicExpressionDialects to be set")
|
||||
}
|
||||
|
||||
if len(props.MessageContentFilterDialects) == 0 {
|
||||
t.Error("Expected MessageContentFilterDialects to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddEventBroker(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
config := &EventBrokerConfig{
|
||||
Address: "mqtt://broker.example.com:1883",
|
||||
TopicPrefix: "onvif/",
|
||||
UserName: "mqtt_user",
|
||||
Password: "mqtt_pass",
|
||||
QoS: 1,
|
||||
}
|
||||
|
||||
err = client.AddEventBroker(ctx, config)
|
||||
if err != nil {
|
||||
t.Fatalf("AddEventBroker failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddEventBrokerValidation(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test nil config.
|
||||
err = client.AddEventBroker(ctx, nil)
|
||||
if err == nil {
|
||||
t.Error("Expected error for nil config")
|
||||
}
|
||||
|
||||
// Test empty address.
|
||||
config := &EventBrokerConfig{Address: ""}
|
||||
err = client.AddEventBroker(ctx, config)
|
||||
if !errors.Is(err, ErrInvalidEventBrokerAddress) {
|
||||
t.Errorf("Expected ErrInvalidEventBrokerAddress, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteEventBroker(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err = client.DeleteEventBroker(ctx, "mqtt://broker.example.com:1883")
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteEventBroker failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteEventBrokerInvalidAddress(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err = client.DeleteEventBroker(ctx, "")
|
||||
if !errors.Is(err, ErrInvalidEventBrokerAddress) {
|
||||
t.Errorf("Expected ErrInvalidEventBrokerAddress, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEventBrokers(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
brokers, err := client.GetEventBrokers(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetEventBrokers failed: %v", err)
|
||||
}
|
||||
|
||||
if len(brokers) == 0 {
|
||||
t.Error("Expected at least one event broker")
|
||||
}
|
||||
|
||||
if len(brokers) > 0 {
|
||||
broker := brokers[0]
|
||||
if broker.Address == "" {
|
||||
t.Error("Expected Address to be set")
|
||||
}
|
||||
|
||||
if broker.TopicPrefix == "" {
|
||||
t.Error("Expected TopicPrefix to be set")
|
||||
}
|
||||
|
||||
if broker.Status == "" {
|
||||
t.Error("Expected Status to be set")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatDuration(t *testing.T) {
|
||||
tests := []struct {
|
||||
duration time.Duration
|
||||
expected string
|
||||
}{
|
||||
{30 * time.Second, "PT30S"},
|
||||
{60 * time.Second, "PT1M"},
|
||||
{90 * time.Second, "PT1M30S"},
|
||||
{5 * time.Minute, "PT5M"},
|
||||
{65 * time.Second, "PT1M5S"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := formatDuration(tt.duration)
|
||||
if result != tt.expected {
|
||||
t.Errorf("formatDuration(%v) = %s, expected %s", tt.duration, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSpaceSeparated(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected []string
|
||||
}{
|
||||
{"", nil},
|
||||
{"mqtt", []string{"mqtt"}},
|
||||
{"mqtt mqtts", []string{"mqtt", "mqtts"}},
|
||||
{" mqtt mqtts ", []string{"mqtt", "mqtts"}},
|
||||
{"a b c", []string{"a", "b", "c"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := splitSpaceSeparated(tt.input)
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("splitSpaceSeparated(%q) returned %d items, expected %d", tt.input, len(result), len(tt.expected))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for i, v := range result {
|
||||
if v != tt.expected[i] {
|
||||
t.Errorf("splitSpaceSeparated(%q)[%d] = %q, expected %q", tt.input, i, v, tt.expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetEventEndpoint(t *testing.T) {
|
||||
server := newMockEventServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, WithCredentials("admin", "password"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
newEndpoint := "http://192.168.1.100/onvif/events"
|
||||
client.SetEventEndpoint(newEndpoint)
|
||||
|
||||
// Verify endpoint was set.
|
||||
endpoint := client.getEventEndpoint()
|
||||
if endpoint != newEndpoint {
|
||||
t.Errorf("Expected event endpoint %s, got %s", newEndpoint, endpoint)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
// Package main tests Event and Device IO services against a real camera.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
onvif "github.com/0x524a/onvif-go"
|
||||
)
|
||||
|
||||
const notAvailable = "N/A"
|
||||
|
||||
func main() {
|
||||
// Command line flags.
|
||||
cameraIP := flag.String("ip", "192.168.1.201", "Camera IP address")
|
||||
username := flag.String("user", "service", "Camera username")
|
||||
password := flag.String("pass", "Service.1234", "Camera password")
|
||||
flag.Parse()
|
||||
|
||||
endpoint := fmt.Sprintf("http://%s/onvif/device_service", *cameraIP)
|
||||
|
||||
fmt.Printf("Testing Event and Device IO services on camera: %s\n", *cameraIP)
|
||||
fmt.Printf("Endpoint: %s\n", endpoint)
|
||||
fmt.Printf("Username: %s\n\n", *username)
|
||||
|
||||
// Create client.
|
||||
client, err := onvif.NewClient(endpoint,
|
||||
onvif.WithCredentials(*username, *password),
|
||||
onvif.WithTimeout(30*time.Second),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create client: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test device information first to verify connectivity.
|
||||
fmt.Println("=== Testing Device Connectivity ===")
|
||||
info, err := client.GetDeviceInformation(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to get device information: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Device: %s %s\n", info.Manufacturer, info.Model)
|
||||
fmt.Printf("Firmware: %s\n", info.FirmwareVersion)
|
||||
fmt.Printf("Serial: %s\n\n", info.SerialNumber)
|
||||
|
||||
// Test Event Service.
|
||||
testEventService(ctx, client)
|
||||
|
||||
// Test Device IO Service.
|
||||
testDeviceIOService(ctx, client)
|
||||
|
||||
fmt.Println("\n=== All Tests Completed ===")
|
||||
}
|
||||
|
||||
func testEventService(ctx context.Context, client *onvif.Client) {
|
||||
fmt.Println("=== Testing Event Service ===")
|
||||
|
||||
// 1. Get Event Service Capabilities.
|
||||
fmt.Println("\n1. GetEventServiceCapabilities")
|
||||
caps, err := client.GetEventServiceCapabilities(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" WSSubscriptionPolicySupport: %v\n", caps.WSSubscriptionPolicySupport)
|
||||
fmt.Printf(" MaxPullPoints: %d\n", caps.MaxPullPoints)
|
||||
fmt.Printf(" PersistentNotificationStorage: %v\n", caps.PersistentNotificationStorage)
|
||||
fmt.Printf(" EventBrokerProtocols: %v\n", caps.EventBrokerProtocols)
|
||||
fmt.Printf(" MaxEventBrokers: %d\n", caps.MaxEventBrokers)
|
||||
}
|
||||
|
||||
// 2. Get Event Properties.
|
||||
fmt.Println("\n2. GetEventProperties")
|
||||
props, err := client.GetEventProperties(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" FixedTopicSet: %v\n", props.FixedTopicSet)
|
||||
fmt.Printf(" TopicNamespaceLocations: %d\n", len(props.TopicNamespaceLocation))
|
||||
fmt.Printf(" TopicExpressionDialects: %d\n", len(props.TopicExpressionDialects))
|
||||
}
|
||||
|
||||
// 3. Create Pull Point Subscription.
|
||||
fmt.Println("\n3. CreatePullPointSubscription")
|
||||
termTime := 60 * time.Second
|
||||
sub, err := client.CreatePullPointSubscription(ctx, "", &termTime, "")
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" SubscriptionReference: %s\n", sub.SubscriptionReference)
|
||||
fmt.Printf(" CurrentTime: %v\n", sub.CurrentTime)
|
||||
fmt.Printf(" TerminationTime: %v\n", sub.TerminationTime)
|
||||
|
||||
// 4. Pull Messages.
|
||||
if sub.SubscriptionReference != "" {
|
||||
fmt.Println("\n4. PullMessages")
|
||||
messages, err := client.PullMessages(ctx, sub.SubscriptionReference, 5*time.Second, 10)
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" Received %d messages\n", len(messages))
|
||||
for i, msg := range messages {
|
||||
if i >= 3 {
|
||||
fmt.Printf(" ... and %d more\n", len(messages)-3)
|
||||
break
|
||||
}
|
||||
|
||||
fmt.Printf(" Message %d: Topic=%s, Operation=%s\n",
|
||||
i+1, msg.Topic, msg.Message.PropertyOperation)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Renew Subscription.
|
||||
fmt.Println("\n5. RenewSubscription")
|
||||
curTime, newTermTime, err := client.RenewSubscription(ctx, sub.SubscriptionReference, 120*time.Second)
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" CurrentTime: %v\n", curTime)
|
||||
fmt.Printf(" NewTerminationTime: %v\n", newTermTime)
|
||||
}
|
||||
|
||||
// 6. Unsubscribe.
|
||||
fmt.Println("\n6. Unsubscribe")
|
||||
err = client.Unsubscribe(ctx, sub.SubscriptionReference)
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR: %v\n", err)
|
||||
} else {
|
||||
fmt.Println(" Successfully unsubscribed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Get Event Brokers (optional, may not be supported).
|
||||
fmt.Println("\n7. GetEventBrokers")
|
||||
brokers, err := client.GetEventBrokers(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR (may not be supported): %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" Found %d event brokers\n", len(brokers))
|
||||
for i, broker := range brokers {
|
||||
fmt.Printf(" Broker %d: %s (Status: %s)\n", i+1, broker.Address, broker.Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDeviceIOService(ctx context.Context, client *onvif.Client) {
|
||||
fmt.Println("\n=== Testing Device IO Service ===")
|
||||
|
||||
// 1. Get Device IO Service Capabilities.
|
||||
fmt.Println("\n1. GetDeviceIOServiceCapabilities")
|
||||
caps, err := client.GetDeviceIOServiceCapabilities(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" VideoSources: %d\n", caps.VideoSources)
|
||||
fmt.Printf(" VideoOutputs: %d\n", caps.VideoOutputs)
|
||||
fmt.Printf(" AudioSources: %d\n", caps.AudioSources)
|
||||
fmt.Printf(" AudioOutputs: %d\n", caps.AudioOutputs)
|
||||
fmt.Printf(" RelayOutputs: %d\n", caps.RelayOutputs)
|
||||
fmt.Printf(" DigitalInputs: %d\n", caps.DigitalInputs)
|
||||
fmt.Printf(" SerialPorts: %d\n", caps.SerialPorts)
|
||||
}
|
||||
|
||||
// 2. Get Digital Inputs.
|
||||
fmt.Println("\n2. GetDigitalInputs")
|
||||
inputs, err := client.GetDigitalInputs(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" Found %d digital inputs\n", len(inputs))
|
||||
for i, input := range inputs {
|
||||
fmt.Printf(" Input %d: Token=%s, IdleState=%s\n", i+1, input.Token, input.IdleState)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Get Video Outputs.
|
||||
fmt.Println("\n3. GetVideoOutputs")
|
||||
outputs, err := client.GetVideoOutputs(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" Found %d video outputs\n", len(outputs))
|
||||
for i, output := range outputs {
|
||||
res := notAvailable
|
||||
if output.Resolution != nil {
|
||||
res = fmt.Sprintf("%dx%d", output.Resolution.Width, output.Resolution.Height)
|
||||
}
|
||||
|
||||
fmt.Printf(" Output %d: Token=%s, Resolution=%s, RefreshRate=%.1f\n",
|
||||
i+1, output.Token, res, output.RefreshRate)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Get Serial Ports.
|
||||
fmt.Println("\n4. GetSerialPorts")
|
||||
ports, err := client.GetSerialPorts(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" Found %d serial ports\n", len(ports))
|
||||
for i, port := range ports {
|
||||
fmt.Printf(" Port %d: Token=%s, Type=%s\n", i+1, port.Token, port.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Get Relay Outputs (using existing method).
|
||||
fmt.Println("\n5. GetRelayOutputs")
|
||||
relays, err := client.GetRelayOutputs(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" Found %d relay outputs\n", len(relays))
|
||||
for i, relay := range relays {
|
||||
mode := notAvailable
|
||||
idleState := notAvailable
|
||||
if relay.Properties.Mode != "" {
|
||||
mode = string(relay.Properties.Mode)
|
||||
}
|
||||
|
||||
if relay.Properties.IdleState != "" {
|
||||
idleState = string(relay.Properties.IdleState)
|
||||
}
|
||||
|
||||
fmt.Printf(" Relay %d: Token=%s, Mode=%s, IdleState=%s\n",
|
||||
i+1, relay.Token, mode, idleState)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user