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 }