1
0
mirror of https://github.com/JoelBender/bacpypes synced 2025-09-28 22:15:23 +08:00
bacpypes/samples/Discover.py
2018-10-15 23:57:01 -04:00

863 lines
29 KiB
Python

#!/usr/bin/python3
"""
Discover
This is a console application to assist with the "discovery" process of finding
BACnet routers, devices, objects, and property values. It reads and/or writes
a tab-delimited property values text file of the values that it has received.
The console commands are:
wirtn - who is router to network
irt - initialize routing table
winn - what is network number
whois - who is
iam - i am
rp - read property
rpm - read property multiple
The property values text file contains these fields:
devid - device identifier
objid - object identifier
propid - property identifier
version - version number
value - value
When the object identifier field is "-" the properties are from I-Am messages
that are received:
address - the BACpypes address of the device
maxAPDULengthAccepted - maximum APDU length accepted
segmentationSupported - segmentation supported
To facilitate finding out what has changed between two different times the
application has been run the property values text file is always sorted.
Whenever a property value is different than the previous value, the version
number is incremented.
For example, the first time the presentValue of analogValue:1 is read the
file could look like this:
202 analogValue:1 presentValue 1 14.02
Then when the application is run again, it could look like this:
202 analogValue:1 presentValue 2 9.52
The application accepts stdin from non-interactive sessions, for example:
$ echo "whois" | python Discover.py stuff
or from a script file:
$ python discover.py stuff << EOF
> whois
> rpm 201 device:201 all
> rpm 201 analogValue:1 all
> EOF
The application prints content during interactive sessions.
"""
import sys
import time
from bacpypes.debugging import bacpypes_debugging, ModuleLogger
from bacpypes.consolelogging import ConfigArgumentParser
from bacpypes.consolecmd import ConsoleCmd
from bacpypes.pdu import Address, LocalBroadcast, GlobalBroadcast
from bacpypes.comm import bind
from bacpypes.core import run, deferred, enable_sleeping
from bacpypes.iocb import IOCB
# application layer
from bacpypes.primitivedata import Unsigned, ObjectIdentifier
from bacpypes.constructeddata import Array
from bacpypes.basetypes import PropertyIdentifier
from bacpypes.object import get_datatype
from bacpypes.app import ApplicationIOController
from bacpypes.appservice import (
StateMachineAccessPoint,
ApplicationServiceAccessPoint,
)
from bacpypes.apdu import (
WhoIsRequest,
IAmRequest,
ReadPropertyRequest,
ReadPropertyACK,
ReadPropertyMultipleRequest,
PropertyReference,
ReadAccessSpecification,
ReadPropertyMultipleACK,
)
# network layer
from bacpypes.netservice import (
NetworkServiceAccessPoint,
NetworkServiceElement,
)
from bacpypes.npdu import (
WhoIsRouterToNetwork,
IAmRouterToNetwork,
InitializeRoutingTable,
InitializeRoutingTableAck,
WhatIsNetworkNumber,
NetworkNumberIs,
)
# IPv4 virtual link layer
from bacpypes.bvllservice import BIPSimple, AnnexJCodec, UDPMultiplexer
# basic objects
from bacpypes.local.device import LocalDeviceObject
# basic services
from bacpypes.service.device import WhoIsIAmServices
from bacpypes.service.object import ReadWritePropertyServices
# some debugging
_debug = 0
_log = ModuleLogger(globals())
# globals
args = None
this_device = None
this_application = None
snapshot = None
# print statements just for interactive
interactive = sys.stdin.isatty()
#
# Snapshot
#
@bacpypes_debugging
class Snapshot:
def __init__(self):
if _debug: Snapshot._debug("__init__")
# empty database
self.data = {}
def read_file(self, filename):
if _debug: Snapshot._debug("read_file %r", filename)
# empty database
self.data = {}
try:
with open(filename) as infile:
lines = infile.readlines()
for line in lines:
devid, objid, propid, version, value = line[:-1].split('\t')
devid = int(devid)
version = int(version)
key = (devid, objid, propid)
self.data[key] = (version, value)
except IOError:
if _debug: Snapshot._debug(" - file not found")
pass
def write_file(self, filename):
if _debug: Snapshot._debug("write_file %r", filename)
data = list(k + v for k, v in self.data.items())
data.sort()
with open(filename, 'w') as outfile:
for row in data:
outfile.write('\t'.join(str(x) for x in row) + '\n')
def upsert(self, devid, objid, propid, value):
if _debug: Snapshot._debug("upsert %r %r %r %r", devid, objid, propid, value)
key = (devid, objid, propid)
if key not in self.data:
if _debug: Snapshot._debug(" - new key")
self.data[key] = (1, value)
else:
version, old_value = self.data[key]
if value != old_value:
if _debug: Snapshot._debug(" - new value")
self.data[key] = (version+1, value)
def get_value(self, devid, objid, propid):
if _debug: Snapshot._debug("get_value %r %r %r", devid, objid, propid)
key = (devid, objid, propid)
if key not in self.data:
return None
else:
return self.data[key][1]
#
# DiscoverNetworkServiceElement
#
@bacpypes_debugging
class DiscoverNetworkServiceElement(NetworkServiceElement):
def __init__(self):
if _debug: DiscoverNetworkServiceElement._debug("__init__")
NetworkServiceElement.__init__(self)
# no pending request
self._request = None
def request(self, adapter, npdu):
if _debug: DiscoverNetworkServiceElement._debug("request %r %r", adapter, npdu)
# save a copy of the request
self._request = npdu
# forward it along
NetworkServiceElement.request(self, adapter, npdu)
def indication(self, adapter, npdu):
if _debug: DiscoverNetworkServiceElement._debug("indication %r %r", adapter, npdu)
if isinstance(npdu, IAmRouterToNetwork):
if interactive:
print("{} router to {}".format(npdu.pduSource, npdu.iartnNetworkList))
elif isinstance(npdu, InitializeRoutingTableAck):
if interactive:
print("{} routing table".format(npdu.pduSource))
for rte in npdu.irtaTable:
print(" {} {} {}".format(rte.rtDNET, rte.rtPortID, rte.rtPortInfo))
elif isinstance(npdu, NetworkNumberIs):
if interactive:
print("{} network number is {}".format(npdu.pduSource, npdu.nniNet))
# forward it along
NetworkServiceElement.indication(self, adapter, npdu)
def response(self, adapter, npdu):
if _debug: DiscoverNetworkServiceElement._debug("response %r %r", adapter, npdu)
# forward it along
NetworkServiceElement.response(self, adapter, npdu)
def confirmation(self, adapter, npdu):
if _debug: DiscoverNetworkServiceElement._debug("confirmation %r %r", adapter, npdu)
# forward it along
NetworkServiceElement.confirmation(self, adapter, npdu)
#
# DiscoverApplication
#
@bacpypes_debugging
class DiscoverApplication(ApplicationIOController, WhoIsIAmServices, ReadWritePropertyServices):
def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None):
if _debug: DiscoverApplication._debug("__init__ %r %r deviceInfoCache=%r aseID=%r", localDevice, localAddress, deviceInfoCache, aseID)
ApplicationIOController.__init__(self, localDevice, localAddress, deviceInfoCache, aseID=aseID)
# local address might be useful for subclasses
if isinstance(localAddress, Address):
self.localAddress = localAddress
else:
self.localAddress = Address(localAddress)
# include a application decoder
self.asap = ApplicationServiceAccessPoint()
# pass the device object to the state machine access point so it
# can know if it should support segmentation
self.smap = StateMachineAccessPoint(localDevice)
# the segmentation state machines need access to the same device
# information cache as the application
self.smap.deviceInfoCache = self.deviceInfoCache
# a network service access point will be needed
self.nsap = NetworkServiceAccessPoint()
# give the NSAP a generic network layer service element
self.nse = DiscoverNetworkServiceElement()
bind(self.nse, self.nsap)
# bind the top layers
bind(self, self.asap, self.smap, self.nsap)
# create a generic BIP stack, bound to the Annex J server
# on the UDP multiplexer
self.bip = BIPSimple()
self.annexj = AnnexJCodec()
self.mux = UDPMultiplexer(self.localAddress)
# bind the bottom layers
bind(self.bip, self.annexj, self.mux.annexJ)
# bind the BIP stack to the network, no network number
self.nsap.bind(self.bip, address=self.localAddress)
# keep track of requests to line up responses
self._request = None
def close_socket(self):
if _debug: DiscoverApplication._debug("close_socket")
# pass to the multiplexer, then down to the sockets
self.mux.close_socket()
def request(self, apdu):
if _debug: DiscoverApplication._debug("request %r", apdu)
# save a copy of the request
self._request = apdu
# forward it along
super(DiscoverApplication, self).request(apdu)
def indication(self, apdu):
if _debug: DiscoverApplication._debug("indication %r", apdu)
# forward it along
super(DiscoverApplication, self).indication(apdu)
def response(self, apdu):
if _debug: DiscoverApplication._debug("response %r", apdu)
# forward it along
super(DiscoverApplication, self).response(apdu)
def confirmation(self, apdu):
if _debug: DiscoverApplication._debug("confirmation %r", apdu)
# forward it along
super(DiscoverApplication, self).confirmation(apdu)
def do_IAmRequest(self, apdu):
if _debug: DiscoverApplication._debug("do_IAmRequest %r", apdu)
if not isinstance(self._request, WhoIsRequest):
if _debug: DiscoverApplication._debug(" - no pending who-is")
return
device_instance = apdu.iAmDeviceIdentifier[1]
if (self._request.deviceInstanceRangeLowLimit is not None) and \
(device_instance < self._request.deviceInstanceRangeLowLimit):
return
if (self._request.deviceInstanceRangeHighLimit is not None) and \
(device_instance > self._request.deviceInstanceRangeHighLimit):
return
# print out something
if interactive:
print("{} @ {}".format(device_instance, apdu.pduSource))
# update the snapshot database
snapshot.upsert(
apdu.iAmDeviceIdentifier[1], '-', 'address',
str(apdu.pduSource),
)
snapshot.upsert(
apdu.iAmDeviceIdentifier[1], '-', 'maxAPDULengthAccepted',
str(apdu.maxAPDULengthAccepted),
)
snapshot.upsert(
apdu.iAmDeviceIdentifier[1], '-', 'segmentationSupported',
apdu.segmentationSupported,
)
#
# DiscoverConsoleCmd
#
@bacpypes_debugging
class DiscoverConsoleCmd(ConsoleCmd):
def do_wirtn(self, args):
"""
wirtn [ <addr> ] [ <net> ]
Send a Who-Is-Router-To-Network message. If <addr> is not specified
the message is locally broadcast.
"""
args = args.split()
if _debug: DiscoverConsoleCmd._debug("do_wirtn %r", args)
# build a request
try:
request = WhoIsRouterToNetwork()
if not args:
request.pduDestination = LocalBroadcast()
elif args[0].isdigit():
request.pduDestination = LocalBroadcast()
request.wirtnNetwork = int(args[0])
else:
request.pduDestination = Address(args[0])
if (len(args) > 1):
request.wirtnNetwork = int(args[1])
except:
print("invalid arguments")
return
# give it to the network service element
this_application.nse.request(this_application.nsap.local_adapter, request)
# sleep for responses
time.sleep(3.0)
def do_irt(self, args):
"""
irt <addr>
Send an empty Initialize-Routing-Table message to an address, a router
will return an acknowledgement with its routing table configuration.
"""
args = args.split()
if _debug: DiscoverConsoleCmd._debug("do_irt %r", args)
# build a request
try:
request = InitializeRoutingTable()
request.pduDestination = Address(args[0])
except:
print("invalid arguments")
return
# give it to the network service element
this_application.nse.request(this_application.nsap.local_adapter, request)
def do_winn(self, args):
"""
winn [ <addr> ]
Send a What-Is-Network-Number message. If the address is unspecified
the message is locally broadcast.
"""
args = args.split()
if _debug: DiscoverConsoleCmd._debug("do_winn %r", args)
# build a request
try:
request = WhatIsNetworkNumber()
if (len(args) > 0):
request.pduDestination = Address(args[0])
else:
request.pduDestination = LocalBroadcast()
except:
print("invalid arguments")
return
# give it to the network service element
this_application.nse.request(this_application.nsap.local_adapter, request)
# sleep for responses
time.sleep(3.0)
def do_whois(self, args):
"""
whois [ <addr> ] [ <lolimit> <hilimit> ]
Send a Who-Is Request and wait 3 seconds for the I-Am "responses" to
be returned.
"""
args = args.split()
if _debug: DiscoverConsoleCmd._debug("do_whois %r", args)
try:
# build a request
request = WhoIsRequest()
if (len(args) == 1) or (len(args) == 3):
request.pduDestination = Address(args[0])
del args[0]
else:
request.pduDestination = GlobalBroadcast()
if len(args) == 2:
request.deviceInstanceRangeLowLimit = int(args[0])
request.deviceInstanceRangeHighLimit = int(args[1])
if _debug: DiscoverConsoleCmd._debug(" - request: %r", request)
# make an IOCB
iocb = IOCB(request)
if _debug: DiscoverConsoleCmd._debug(" - iocb: %r", iocb)
# give it to the application
this_application.request_io(iocb)
# sleep for responses
time.sleep(3.0)
except Exception as err:
DiscoverConsoleCmd._exception("exception: %r", err)
def do_iam(self, args):
"""
iam [ <addr> ]
Send an I-Am request. If the address is unspecified the message is
locally broadcast.
"""
args = args.split()
if _debug: DiscoverConsoleCmd._debug("do_iam %r", args)
try:
# build a request
request = IAmRequest()
if (len(args) == 1):
request.pduDestination = Address(args[0])
else:
request.pduDestination = GlobalBroadcast()
# set the parameters from the device object
request.iAmDeviceIdentifier = this_device.objectIdentifier
request.maxAPDULengthAccepted = this_device.maxApduLengthAccepted
request.segmentationSupported = this_device.segmentationSupported
request.vendorID = this_device.vendorIdentifier
if _debug: DiscoverConsoleCmd._debug(" - request: %r", request)
# make an IOCB
iocb = IOCB(request)
if _debug: DiscoverConsoleCmd._debug(" - iocb: %r", iocb)
# give it to the application
this_application.request_io(iocb)
except Exception as err:
DiscoverConsoleCmd._exception("exception: %r", err)
def do_rp(self, args):
"""
rp <devid> <objid> <prop> [ <indx> ]
Send a Read-Property request to a device identified by its device
identifier.
"""
args = args.split()
if _debug: DiscoverConsoleCmd._debug("do_rp %r", args)
try:
devid, obj_id, prop_id = args[:3]
devid = int(devid)
obj_id = ObjectIdentifier(obj_id).value
datatype = get_datatype(obj_id[0], prop_id)
if not datatype:
raise ValueError("invalid property for object type")
# map the devid identifier to an address from the database
addr = snapshot.get_value(devid, '-', 'address')
if not addr:
raise ValueError("unknown device")
if _debug: DiscoverConsoleCmd._debug(" - addr: %r", addr)
# build a request
request = ReadPropertyRequest(
objectIdentifier=obj_id,
propertyIdentifier=prop_id,
)
request.pduDestination = Address(addr)
if len(args) == 4:
request.propertyArrayIndex = int(args[3])
if _debug: DiscoverConsoleCmd._debug(" - request: %r", request)
# make an IOCB
iocb = IOCB(request)
if _debug: DiscoverConsoleCmd._debug(" - iocb: %r", iocb)
# give it to the application
this_application.request_io(iocb)
# wait for it to complete
iocb.wait()
# do something for error/reject/abort
if iocb.ioError:
if interactive:
print(str(iocb.ioError))
# do something for success
elif iocb.ioResponse:
apdu = iocb.ioResponse
# should be an ack
if not isinstance(apdu, ReadPropertyACK):
if _debug: DiscoverConsoleCmd._debug(" - not an ack")
return
# find the datatype
datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier)
if _debug: DiscoverConsoleCmd._debug(" - datatype: %r", datatype)
if not datatype:
raise TypeError("unknown datatype")
# special case for array parts, others are managed by cast_out
if issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None):
if apdu.propertyArrayIndex == 0:
datatype = Unsigned
else:
datatype = datatype.subtype
if _debug: DiscoverConsoleCmd._debug(" - datatype: %r", datatype)
value = apdu.propertyValue.cast_out(datatype)
if _debug: DiscoverConsoleCmd._debug(" - value: %r", value)
# convert the value to a string
if hasattr(value, 'dict_contents'):
dict_contents = value.dict_contents(as_class=OrderedDict)
str_value = json.dumps(dict_contents)
else:
str_value = str(value)
if interactive:
print(str_value)
# save it in the snapshot
snapshot.upsert(devid, '{}:{}'.format(*obj_id), prop_id, str_value)
# do something with nothing?
else:
if _debug: DiscoverConsoleCmd._debug(" - ioError or ioResponse expected")
except Exception as error:
DiscoverConsoleCmd._exception("exception: %r", error)
def do_rpm(self, args):
"""
rpm <devid> ( <objid> ( <prop> [ <indx> ] )... )...
Send a Read-Property-Multiple request to a device identified by its
device identifier.
"""
args = args.split()
if _debug: DiscoverConsoleCmd._debug("do_rpm %r", args)
try:
i = 0
devid = int(args[i])
if _debug: DiscoverConsoleCmd._debug(" - devid: %r", devid)
i += 1
# map the devid identifier to an address from the database
addr = snapshot.get_value(devid, '-', 'address')
if not addr:
raise ValueError("unknown device")
if _debug: DiscoverConsoleCmd._debug(" - addr: %r", addr)
read_access_spec_list = []
while i < len(args):
obj_id = ObjectIdentifier(args[i]).value
if _debug: DiscoverConsoleCmd._debug(" - obj_id: %r", obj_id)
i += 1
prop_reference_list = []
while i < len(args):
prop_id = args[i]
if _debug: DiscoverConsoleCmd._debug(" - prop_id: %r", prop_id)
if prop_id not in PropertyIdentifier.enumerations:
break
i += 1
if prop_id in ('all', 'required', 'optional'):
pass
else:
datatype = get_datatype(obj_id[0], prop_id)
if not datatype:
raise ValueError("invalid property for object type")
# build a property reference
prop_reference = PropertyReference(
propertyIdentifier=prop_id,
)
# check for an array index
if (i < len(args)) and args[i].isdigit():
prop_reference.propertyArrayIndex = int(args[i])
i += 1
# add it to the list
prop_reference_list.append(prop_reference)
# check for at least one property
if not prop_reference_list:
raise ValueError("provide at least one property")
# build a read access specification
read_access_spec = ReadAccessSpecification(
objectIdentifier=obj_id,
listOfPropertyReferences=prop_reference_list,
)
# add it to the list
read_access_spec_list.append(read_access_spec)
# check for at least one
if not read_access_spec_list:
raise RuntimeError("at least one read access specification required")
# build the request
request = ReadPropertyMultipleRequest(
listOfReadAccessSpecs=read_access_spec_list,
)
request.pduDestination = Address(addr)
if _debug: DiscoverConsoleCmd._debug(" - request: %r", request)
# make an IOCB
iocb = IOCB(request)
if _debug: DiscoverConsoleCmd._debug(" - iocb: %r", iocb)
# give it to the application
this_application.request_io(iocb)
# wait for it to complete
iocb.wait()
# do something for success
if iocb.ioResponse:
apdu = iocb.ioResponse
# should be an ack
if not isinstance(apdu, ReadPropertyMultipleACK):
if _debug: DiscoverConsoleCmd._debug(" - not an ack")
return
# loop through the results
for result in apdu.listOfReadAccessResults:
# here is the object identifier
objectIdentifier = result.objectIdentifier
if _debug: DiscoverConsoleCmd._debug(" - objectIdentifier: %r", objectIdentifier)
# now come the property values per object
for element in result.listOfResults:
# get the property and array index
propertyIdentifier = element.propertyIdentifier
if _debug: DiscoverConsoleCmd._debug(" - propertyIdentifier: %r", propertyIdentifier)
propertyArrayIndex = element.propertyArrayIndex
if _debug: DiscoverConsoleCmd._debug(" - propertyArrayIndex: %r", propertyArrayIndex)
# here is the read result
readResult = element.readResult
property_label = str(propertyIdentifier)
if propertyArrayIndex is not None:
property_label += "[" + str(propertyArrayIndex) + "]"
# check for an error
if readResult.propertyAccessError is not None:
if interactive:
print("{} ! {}".format(property_label, readResult.propertyAccessError))
else:
# here is the value
propertyValue = readResult.propertyValue
# find the datatype
datatype = get_datatype(objectIdentifier[0], propertyIdentifier)
if _debug: DiscoverConsoleCmd._debug(" - datatype: %r", datatype)
if not datatype:
str_value = '?'
else:
# special case for array parts, others are managed by cast_out
if issubclass(datatype, Array) and (propertyArrayIndex is not None):
if propertyArrayIndex == 0:
datatype = Unsigned
else:
datatype = datatype.subtype
if _debug: DiscoverConsoleCmd._debug(" - datatype: %r", datatype)
value = propertyValue.cast_out(datatype)
if _debug: DiscoverConsoleCmd._debug(" - value: %r", value)
# convert the value to a string
if hasattr(value, 'dict_contents'):
dict_contents = value.dict_contents(as_class=OrderedDict)
str_value = json.dumps(dict_contents)
else:
str_value = str(value)
if interactive:
print("{}: {}".format(property_label, str_value))
# save it in the snapshot
snapshot.upsert(devid, '{}:{}'.format(*objectIdentifier), propertyIdentifier, str_value)
# do something for error/reject/abort
if iocb.ioError:
if interactive:
print(str(iocb.ioError))
except Exception as error:
DiscoverConsoleCmd._exception("exception: %r", error)
#
# __main__
#
def main():
global args, this_device, this_application, snapshot
# parse the command line arguments
parser = ConfigArgumentParser(description=__doc__)
# input file for exiting configuration
parser.add_argument("infile",
default="-",
help="input file",
)
# output file for discovered configuration
parser.add_argument("outfile", nargs='?',
default='-unspecified-',
help="output file",
)
args = parser.parse_args()
if _debug: _log.debug("initialization")
if _debug: _log.debug(" - args: %r", args)
# make a device object
this_device = LocalDeviceObject(ini=args.ini)
if _debug: _log.debug(" - this_device: %r", this_device)
# make a simple application
this_application = DiscoverApplication(
this_device, args.ini.address,
)
if _debug: _log.debug(" - this_application: %r", this_application)
# make a snapshot 'database'
snapshot = Snapshot()
# read in an existing snapshot
if args.infile != '-':
snapshot.read_file(args.infile)
# make a console
this_console = DiscoverConsoleCmd()
_log.debug(" - this_console: %r", this_console)
# enable sleeping will help with threads
enable_sleeping()
_log.debug("running")
run()
_log.debug("fini")
# write out the snapshot, outfile defaults to infile if not specified
if args.outfile == '-unspecified-':
args.outfile = args.infile
if args.outfile != '-':
snapshot.write_file(args.outfile)
if __name__ == "__main__":
main()