blob: 8da833688973d8b8c76b2ad6ce2acef0b3a2c28f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package bacnetip
import (
"context"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/IBM/netaddr"
"github.com/libp2p/go-reuseport"
"github.com/pkg/errors"
"github.com/rs/zerolog"
apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
driverModel "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
spiModel "github.com/apache/plc4x/plc4go/spi/model"
"github.com/apache/plc4x/plc4go/spi/options"
"github.com/apache/plc4x/plc4go/spi/utils"
)
type Discoverer struct {
wg sync.WaitGroup // use to track spawned go routines
passLogToModel bool
log zerolog.Logger
}
func NewDiscoverer(_options ...options.WithOption) *Discoverer {
passLoggerToModel, _ := options.ExtractPassLoggerToModel(_options...)
customLogger := options.ExtractCustomLoggerOrDefaultToGlobal(_options...)
return &Discoverer{
passLogToModel: passLoggerToModel,
log: customLogger,
}
}
func (d *Discoverer) Discover(ctx context.Context, callback func(event apiModel.PlcDiscoveryItem), discoveryOptions ...options.WithDiscoveryOption) error {
// TODO: handle ctx
interfaces, err := extractInterfaces(discoveryOptions)
if err != nil {
return errors.Wrap(err, "error extracting interfaces")
}
specificOptions, err := extractProtocolSpecificOptions(discoveryOptions)
if err != nil {
return errors.Wrap(err, "error extracting protocol specific options")
}
communicationChannels, err := d.buildupCommunicationChannels(ctx, interfaces, specificOptions.bacNetPort)
if err != nil {
return errors.Wrap(err, "error building communication channels")
}
// TODO: make adjustable
ctx, cancelFunc := context.WithTimeout(ctx, time.Second*60)
defer func() {
cancelFunc()
}()
incomingBVLCChannel, err := d.broadcastAndDiscover(ctx, communicationChannels, specificOptions)
if err != nil {
return errors.Wrap(err, "error broadcasting and discovering")
}
d.handleIncomingBVLCs(ctx, callback, incomingBVLCChannel)
// TODO: make adjustable
time.Sleep(time.Second * 60)
for _, channel := range communicationChannels {
_ = channel.Close()
}
return nil
}
func (d *Discoverer) broadcastAndDiscover(ctx context.Context, communicationChannels []communicationChannel, specificOptions *protocolSpecificOptions) (chan receivedBvlcMessage, error) {
incomingBVLCChannel := make(chan receivedBvlcMessage)
for _, communicationChannelInstance := range communicationChannels {
if err := ctx.Err(); err != nil {
return incomingBVLCChannel, err
}
// Prepare the discovery packet data
{
var lowLimit driverModel.BACnetContextTagUnsignedInteger
var highLimit driverModel.BACnetContextTagUnsignedInteger
if whoIsOptions := specificOptions.whoIsOptions; whoIsOptions != nil && whoIsOptions.limits != nil {
lowLimit = driverModel.CreateBACnetContextTagUnsignedInteger(0, whoIsOptions.limits.low)
highLimit = driverModel.CreateBACnetContextTagUnsignedInteger(1, whoIsOptions.limits.high)
}
requestWhoIs := driverModel.NewBACnetUnconfirmedServiceRequestWhoIs(lowLimit, highLimit)
apdu := driverModel.NewAPDUUnconfirmedRequest(requestWhoIs)
control := driverModel.NewNPDUControl(false, false, false, false, driverModel.NPDUNetworkPriority_NORMAL_MESSAGE)
npdu := driverModel.NewNPDU(1, control, nil, nil, nil, nil, nil, nil, nil, nil, apdu)
bvlc := driverModel.NewBVLCOriginalUnicastNPDU(npdu)
// Send the search request.
theBytes, err := bvlc.Serialize()
if err != nil {
return nil, err
}
if _, err := communicationChannelInstance.broadcastConnection.WriteTo(theBytes, communicationChannelInstance.broadcastConnection.LocalAddr()); err != nil {
d.log.Debug().Err(err).Msg("Error sending broadcast")
}
}
if whoHasOptions := specificOptions.whoHasOptions; whoHasOptions != nil {
var lowLimit driverModel.BACnetContextTagUnsignedInteger
var highLimit driverModel.BACnetContextTagUnsignedInteger
if limits := whoHasOptions.limits; limits != nil {
lowLimit = driverModel.CreateBACnetContextTagUnsignedInteger(0, limits.deviceInstanceRangeLow)
highLimit = driverModel.CreateBACnetContextTagUnsignedInteger(1, limits.deviceInstanceRangeHigh)
}
var object driverModel.BACnetUnconfirmedServiceRequestWhoHasObject
if identifier := whoHasOptions.object.identifier; identifier != nil {
var objectType uint16
objectTypeByName, ok := driverModel.BACnetObjectTypeByName(identifier.type_)
if ok {
parseUint, err := strconv.ParseUint(identifier.type_, 10, 16)
if err != nil {
return nil, err
}
objectType = uint16(parseUint)
} else {
objectType = uint16(objectTypeByName)
}
objectIdentifier := driverModel.CreateBACnetContextTagObjectIdentifier(2, objectType, uint32(identifier.instance))
object = driverModel.NewBACnetUnconfirmedServiceRequestWhoHasObjectIdentifier(objectIdentifier.GetHeader(), objectIdentifier)
} else if name := whoHasOptions.object.name; name != nil {
characterString := driverModel.CreateBACnetContextTagCharacterString(3, driverModel.BACnetCharacterEncoding_ISO_10646, *name)
object = driverModel.NewBACnetUnconfirmedServiceRequestWhoHasObjectName(characterString.GetHeader(), characterString)
} else {
panic("Invalid state")
}
requestWhoHas := driverModel.NewBACnetUnconfirmedServiceRequestWhoHas(lowLimit, highLimit, object)
apdu := driverModel.NewAPDUUnconfirmedRequest(requestWhoHas)
control := driverModel.NewNPDUControl(false, false, false, false, driverModel.NPDUNetworkPriority_NORMAL_MESSAGE)
npdu := driverModel.NewNPDU(1, control, nil, nil, nil, nil, nil, nil, nil, nil, apdu)
bvlc := driverModel.NewBVLCOriginalUnicastNPDU(npdu)
// Send the search request.
theBytes, err := bvlc.Serialize()
if err != nil {
return nil, err
}
if _, err := communicationChannelInstance.broadcastConnection.WriteTo(theBytes, communicationChannelInstance.broadcastConnection.LocalAddr()); err != nil {
d.log.Debug().Err(err).Msg("Error sending broadcast")
}
}
d.wg.Go(func() {
for {
if err := ctx.Err(); err != nil {
d.log.Debug().Err(err).Msg("ending")
return
}
blockingReadChan := make(chan bool)
d.wg.Go(func() {
buf := make([]byte, 4096)
n, addr, err := communicationChannelInstance.unicastConnection.ReadFrom(buf)
if err != nil {
d.log.Debug().Err(err).Msg("Ending unicast receive")
blockingReadChan <- false
return
}
d.log.Debug().Stringer("addr", addr).Msg("Received broadcast bvlc")
ctxForModel := options.GetLoggerContextForModel(ctx, d.log, options.WithPassLoggerToModel(d.passLogToModel))
incomingBvlc, err := driverModel.BVLCParse[driverModel.BVLC](ctxForModel, buf[:n])
if err != nil {
d.log.Warn().Err(err).Msg("Could not parse bvlc")
blockingReadChan <- true
return
}
incomingBVLCChannel <- receivedBvlcMessage{incomingBvlc, addr}
blockingReadChan <- true
})
select {
case ok := <-blockingReadChan:
if !ok {
d.log.Debug().Msg("Ending unicast reading")
return
}
d.log.Trace().Msg("Received something unicast")
case <-ctx.Done():
d.log.Debug().Err(ctx.Err()).Msg("Ending unicast receive")
return
}
}
})
d.wg.Go(func() {
for {
if err := ctx.Err(); err != nil {
d.log.Debug().Err(err).Msg("ending")
return
}
blockingReadChan := make(chan bool)
d.wg.Go(func() {
buf := make([]byte, 4096)
n, addr, err := communicationChannelInstance.broadcastConnection.ReadFrom(buf)
if err != nil {
d.log.Debug().Err(err).Msg("Ending broadcast receive")
blockingReadChan <- false
return
}
d.log.Debug().Stringer("addr", addr).Msg("Received broadcast bvlc")
ctxForModel := options.GetLoggerContextForModel(ctx, d.log, options.WithPassLoggerToModel(d.passLogToModel))
incomingBvlc, err := driverModel.BVLCParse[driverModel.BVLC](ctxForModel, buf[:n])
if err != nil {
d.log.Warn().Err(err).Msg("Could not parse bvlc")
blockingReadChan <- true
}
incomingBVLCChannel <- receivedBvlcMessage{incomingBvlc, addr}
blockingReadChan <- true
})
select {
case ok := <-blockingReadChan:
if !ok {
d.log.Debug().Msg("Ending broadcast reading")
return
}
d.log.Trace().Msg("Received something broadcast")
case <-ctx.Done():
d.log.Debug().Err(ctx.Err()).Msg("Ending broadcast receive")
return
}
}
})
}
return incomingBVLCChannel, nil
}
func (d *Discoverer) handleIncomingBVLCs(ctx context.Context, callback func(event apiModel.PlcDiscoveryItem), incomingBVLCChannel chan receivedBvlcMessage) {
for {
if err := ctx.Err(); err != nil {
// TODO: maybe we log something, but maybe it is fine
return
}
select {
case receivedBvlc := <-incomingBVLCChannel:
var npdu driverModel.NPDU
if bvlc, ok := receivedBvlc.bvlc.(interface{ GetNpdu() driverModel.NPDU }); ok {
npdu = bvlc.GetNpdu()
}
_ = npdu
if apdu := npdu.GetApdu(); apdu == nil {
nlm := npdu.GetNlm()
d.log.Debug().Stringer("nlm", nlm).Msg("Got nlm")
continue
}
apdu := npdu.GetApdu()
if _, ok := apdu.(driverModel.APDUConfirmedRequest); ok {
d.log.Debug().Stringer("apdu", apdu).Msg("Got apdu")
continue
}
apduUnconfirmedRequest := apdu.(driverModel.APDUUnconfirmedRequest)
serviceRequest := apduUnconfirmedRequest.GetServiceRequest()
switch serviceRequest := serviceRequest.(type) {
case driverModel.BACnetUnconfirmedServiceRequestIAm:
iAm := serviceRequest
remoteUrl, err := url.Parse("udp://" + receivedBvlc.addr.String())
if err != nil {
d.log.Debug().Err(err).Msg("Error parsing url")
}
discoveryEvent := spiModel.NewDefaultPlcDiscoveryItem(
"bacnet-ip",
"udp",
*remoteUrl,
nil,
fmt.Sprintf("device %v:%v", iAm.GetDeviceIdentifier().GetObjectType(), iAm.GetDeviceIdentifier().GetInstanceNumber()),
nil,
)
// Pass the event back to the callback
callback(discoveryEvent)
case driverModel.BACnetUnconfirmedServiceRequestIHave:
iHave := serviceRequest
remoteUrl, err := url.Parse("udp://" + receivedBvlc.addr.String())
if err != nil {
d.log.Debug().Err(err).Msg("Error parsing url")
}
discoveryEvent := spiModel.NewDefaultPlcDiscoveryItem(
"bacnet-ip",
"udp",
*remoteUrl,
nil,
fmt.Sprintf("device %v:%v with %v:%v and %v", iHave.GetDeviceIdentifier().GetObjectType(), iHave.GetDeviceIdentifier().GetInstanceNumber(), iHave.GetObjectIdentifier().GetObjectType(), iHave.GetObjectIdentifier().GetInstanceNumber(), iHave.GetObjectName().GetValue()),
nil,
)
// Pass the event back to the callback
callback(discoveryEvent)
}
case <-ctx.Done():
d.log.Debug().Err(ctx.Err()).Msg("Ending unicast receive")
return
}
}
}
func (d *Discoverer) buildupCommunicationChannels(ctx context.Context, interfaces []net.Interface, bacNetPort int) (communicationChannels []communicationChannel, err error) {
// Iterate over all network devices of this system.
for _, networkInterface := range interfaces {
if err := ctx.Err(); err != nil {
return nil, err
}
unicastInterfaceAddress, err := networkInterface.Addrs()
if err != nil {
return nil, errors.Wrapf(err, "Error getting Addresses for %v", networkInterface)
}
// Iterate over all addresses the current interface has configured
for _, unicastAddress := range unicastInterfaceAddress {
if err := ctx.Err(); err != nil {
return nil, err
}
var ipAddr net.IP
switch addr := unicastAddress.(type) {
// If the device is configured to communicate with a subnet
case *net.IPNet:
ipAddr = addr.IP.To4()
if ipAddr == nil {
// TODO: for now we only support ipv4 (reuse doesn't like v6 address strings atm)
continue
ipAddr = addr.IP.To16()
}
// If the device is configured for a point-to-point connection
case *net.IPAddr:
ipAddr = addr.IP.To4()
if ipAddr == nil {
// TODO: for now we only support ipv4 (reuse doesn't like v6 address strings atm)
continue
ipAddr = addr.IP.To16()
}
default:
continue
}
if !ipAddr.IsGlobalUnicast() {
continue
}
// Handle undirected
unicastConnection, err := reuseport.ListenPacket("udp4", fmt.Sprintf("%v:%d", ipAddr, bacNetPort))
if err != nil {
d.log.Debug().Err(err).Msg("Error building unicast Port")
continue
}
_, cidr, _ := net.ParseCIDR(unicastAddress.String())
broadcastAddr := netaddr.BroadcastAddr(cidr)
// Handle undirected
broadcastConnection, err := reuseport.ListenPacket("udp4", fmt.Sprintf("%v:%d", broadcastAddr, bacNetPort))
if err != nil {
if err := unicastConnection.Close(); err != nil {
d.log.Debug().Err(err).Msg("Error closing transport instance")
}
d.log.Debug().Err(err).Msg("Error building broadcast Port")
continue
}
communicationChannels = append(communicationChannels, communicationChannel{
networkInterface: networkInterface,
unicastConnection: unicastConnection,
broadcastConnection: broadcastConnection,
log: d.log,
})
}
}
return
}
func (d *Discoverer) Close() error {
defer utils.StopWarn(d.log)()
d.log.Trace().Msg("Waiting for goroutines to stop")
d.wg.Wait()
return nil
}
type receivedBvlcMessage struct {
bvlc driverModel.BVLC
addr net.Addr
}
type communicationChannel struct {
networkInterface net.Interface
unicastConnection net.PacketConn
broadcastConnection net.PacketConn
log zerolog.Logger
}
func (c communicationChannel) Close() error {
defer utils.StopWarn(c.log)()
_ = c.unicastConnection.Close()
_ = c.broadcastConnection.Close()
return nil
}
func extractInterfaces(discoveryOptions []options.WithDiscoveryOption) ([]net.Interface, error) {
allInterfaces, err := net.Interfaces()
if err != nil {
return nil, err
}
// If no device is explicitly selected via option, simply use all of them
// However if a discovery option is present to select a device by name, only
// add those devices matching any of the given names.
var interfaces []net.Interface
deviceNames := options.FilterDiscoveryOptionsDeviceName(discoveryOptions)
if len(deviceNames) > 0 {
for _, curInterface := range allInterfaces {
for _, deviceNameOption := range deviceNames {
if curInterface.Name == deviceNameOption.GetDeviceName() {
interfaces = append(interfaces, curInterface)
break
}
}
}
} else {
interfaces = allInterfaces
}
return interfaces, nil
}
type protocolSpecificOptions struct {
bacNetPort int
whoIsOptions *struct {
limits *struct {
low uint
high uint
}
}
whoHasOptions *struct {
limits *struct {
deviceInstanceRangeLow uint
deviceInstanceRangeHigh uint
}
object struct {
identifier *struct {
type_ string
instance uint
}
name *string
}
}
}
func bacNetPort(port int) option {
return func(specificOptions *protocolSpecificOptions) error {
specificOptions.bacNetPort = port
return nil
}
}
func whoIsLimits(whoIsLowLimit, whoIsHighLimit uint) option {
return func(specificOptions *protocolSpecificOptions) error {
specificOptions.whoIsOptions = &struct {
limits *struct {
low uint
high uint
}
}{&struct {
low uint
high uint
}{whoIsLowLimit, whoIsHighLimit}}
return nil
}
}
func whoHasOption() option {
return func(specificOptions *protocolSpecificOptions) error {
specificOptions.whoHasOptions = &struct {
limits *struct {
deviceInstanceRangeLow uint
deviceInstanceRangeHigh uint
}
object struct {
identifier *struct {
type_ string
instance uint
}
name *string
}
}{}
return nil
}
}
func whoHasLimits(whoHasDeviceInstanceRangeLowLimit, whoHasDeviceInstanceRangeHighLimit uint) option {
return func(specificOptions *protocolSpecificOptions) error {
if specificOptions.whoHasOptions == nil {
panic("we should have set this before")
}
specificOptions.whoHasOptions.limits = &struct {
deviceInstanceRangeLow uint
deviceInstanceRangeHigh uint
}{whoHasDeviceInstanceRangeLowLimit, whoHasDeviceInstanceRangeHighLimit}
return nil
}
}
func whoHasObjectIdentifier(objectIdentifierType string, objectIdentifierInstance uint) option {
return func(specificOptions *protocolSpecificOptions) error {
if specificOptions.whoHasOptions == nil {
panic("we should have set this before")
}
specificOptions.whoHasOptions.object.identifier = &struct {
type_ string
instance uint
}{objectIdentifierType, objectIdentifierInstance}
return nil
}
}
func whoHasObjectName(objectName string) option {
return func(specificOptions *protocolSpecificOptions) error {
if specificOptions.whoHasOptions == nil {
panic("we should have set this before")
}
specificOptions.whoHasOptions.object.name = &objectName
return nil
}
}
func newProtocolSpecificOptions(options ...option) (*protocolSpecificOptions, error) {
var specificOptions protocolSpecificOptions
for _, _option := range options {
if parseErr := _option(&specificOptions); parseErr != nil {
return nil, parseErr
}
}
return &specificOptions, nil
}
type option func(specificOptions *protocolSpecificOptions) error
func extractProtocolSpecificOptions(discoveryOptions []options.WithDiscoveryOption) (*protocolSpecificOptions, error) {
var collectedOptions []option
filteredOptionMap := make(map[string][]any)
for _, protocolSpecificOption := range options.FilterDiscoveryOptionProtocolSpecific(discoveryOptions) {
key := protocolSpecificOption.GetKey()
value := protocolSpecificOption.GetValue()
if _, ok := filteredOptionMap[key]; !ok {
filteredOptionMap[key] = make([]any, 0)
}
filteredOptionMap[key] = append(filteredOptionMap[key], value)
}
keyDependencies := map[string][]struct {
key string
mustBePresent bool
}{
"who-is-low-limit": {{"who-is-high-limit", true}},
"who-is-high-limit": {{"who-is-low-limit", true}},
"who-has-device-instance-range-low-limit": {{"who-has-device-instance-range-high-limit", true}, {"who-has-object*", true}},
"who-has-device-instance-range-high-limit": {{"who-has-device-instance-range-low-limit", true}, {"who-has-object*", true}},
"who-has-object-identifier-type": {{"who-has-object-identifier-instance", true}, {"who-has-object-name", false}},
"who-has-object-identifier-instance": {{"who-has-object-identifier-type", true}, {"who-has-object-name", false}},
"who-has-object-name": {{"who-has-object-identifier-instance", false}, {"who-has-object-identifier-type", false}},
}
for key, value := range keyDependencies {
if _, ok := filteredOptionMap[key]; ok {
for _, otherKey := range value {
if strings.HasSuffix(otherKey.key, "*") {
prefix := strings.TrimSuffix(otherKey.key, "*")
mustBePresent := otherKey.mustBePresent
var found bool
for key := range filteredOptionMap {
found = found || strings.HasPrefix(key, prefix)
}
if mustBePresent && !found {
return nil, errors.Errorf("When %s is set one of %s must also be set", key, otherKey.key)
} else if !mustBePresent && found {
return nil, errors.Errorf("When %s is set none of %s must be set", key, otherKey.key)
}
} else if _, otherOk := filteredOptionMap[otherKey.key]; otherOk && !otherKey.mustBePresent {
return nil, errors.Errorf("When %s is set %s must not be set", key, otherKey.key)
} else if !otherOk && otherKey.mustBePresent {
return nil, errors.Errorf("When %s is set %s must be set too", key, otherKey.key)
}
}
}
}
if _, ok := filteredOptionMap["bacnet-port"]; ok {
parsedInt, err := OneInt(filteredOptionMap, "bacnet-port")
if err != nil {
return nil, err
}
collectedOptions = append(collectedOptions, bacNetPort(parsedInt))
} else {
collectedOptions = append(collectedOptions, bacNetPort(47808))
}
if whoIsLow, whoIsHigh, ok, err := func() (whoIsLowLimit uint, whoIsHighLimit uint, ok bool, err error) {
if _, limitPresent := filteredOptionMap["who-is-low-limit"]; !limitPresent {
return
}
ok = true
whoIsLowLimit, err = OneUint(filteredOptionMap, "who-is-low-limit")
whoIsHighLimit, err = OneUint(filteredOptionMap, "who-is-high-limit")
return
}(); ok {
collectedOptions = append(collectedOptions, whoIsLimits(whoIsLow, whoIsHigh))
} else if err != nil {
return nil, err
}
for key := range filteredOptionMap {
if strings.HasPrefix(key, "who-has-object") {
collectedOptions = append(collectedOptions, whoHasOption())
break
}
}
if whoHasDeviceInstanceRangeLowLimit, whoHasDeviceInstanceRangeHighLimit, ok, err := func() (whoIsLowLimit uint, whoIsHighLimit uint, ok bool, err error) {
if _, limitPresent := filteredOptionMap["who-has-device-instance-range-low-limit"]; !limitPresent {
return
}
ok = true
whoIsLowLimit, err = OneUint(filteredOptionMap, "who-has-device-instance-range-low-limit")
whoIsHighLimit, err = OneUint(filteredOptionMap, "who-has-device-instance-range-high-limit")
return
}(); ok {
collectedOptions = append(collectedOptions, whoHasLimits(whoHasDeviceInstanceRangeLowLimit, whoHasDeviceInstanceRangeHighLimit))
} else if err != nil {
return nil, err
}
if whoHasObjectIdentifierType, objectIdentifierInstance, ok, err := func() (whoHasObjectIdentifierType string, whoHasObjectIdentifierInstance uint, ok bool, err error) {
if _, limitPresent := filteredOptionMap["who-has-object-identifier-type"]; !limitPresent {
return
}
ok = true
whoHasObjectIdentifierType, err = OneString(filteredOptionMap, "who-has-object-identifier-type")
whoHasObjectIdentifierInstance, err = OneUint(filteredOptionMap, "who-has-object-identifier-instance")
return
}(); ok {
collectedOptions = append(collectedOptions, whoHasObjectIdentifier(whoHasObjectIdentifierType, objectIdentifierInstance))
} else if err != nil {
return nil, err
}
if _, ok := filteredOptionMap["who-has-object-name"]; ok {
if name, err := OneString(filteredOptionMap, "who-has-object-name"); err != nil {
return nil, err
} else {
collectedOptions = append(collectedOptions, whoHasObjectName(name))
}
}
return newProtocolSpecificOptions(collectedOptions...)
}
func OneInt(filteredOptionMap map[string][]any, key string) (int, error) {
value, err := One(filteredOptionMap, key)
if err != nil {
return 0, err
}
parsedInt, err := strconv.ParseInt(fmt.Sprintf("%v", value), 10, 32)
if err != nil {
return 0, errors.Wrap(err, "Error parsing option bacnet-port")
}
return int(parsedInt), nil
}
func OneUint(filteredOptionMap map[string][]any, key string) (uint, error) {
value, err := One(filteredOptionMap, key)
if err != nil {
return 0, err
}
parsedInt, err := strconv.ParseUint(fmt.Sprintf("%v", value), 10, 32)
if err != nil {
return 0, errors.Wrap(err, "Error parsing option bacnet-port")
}
return uint(parsedInt), nil
}
func OneString(filteredOptionMap map[string][]any, key string) (string, error) {
value, err := One(filteredOptionMap, key)
if err != nil {
return "", err
}
return fmt.Sprintf("%v", value), nil
}
func One(filteredOptionMap map[string][]any, key string) (any, error) {
values := filteredOptionMap[key]
if len(values) != 1 {
return nil, errors.Errorf("%s expects only one value", key)
}
return values[0], nil
}