From 6fe4dc194cccbc1df86b6259efe63ef19ab61b93 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Sun, 8 Oct 2017 00:07:21 -0400 Subject: [PATCH 1/3] version 0.16.4 released --- py25/bacpypes/__init__.py | 2 +- py25/bacpypes/app.py | 11 + py25/bacpypes/constructeddata.py | 5 + py25/bacpypes/pdu.py | 4 +- py25/bacpypes/primitivedata.py | 14 +- py27/bacpypes/__init__.py | 2 +- py27/bacpypes/app.py | 11 + py27/bacpypes/constructeddata.py | 5 + py27/bacpypes/pdu.py | 4 +- py27/bacpypes/primitivedata.py | 12 +- py34/bacpypes/__init__.py | 2 +- py34/bacpypes/app.py | 11 + py34/bacpypes/constructeddata.py | 5 + samples/MultipleReadPropertyHammer.py | 229 ++++++++++++++++++ samples/RandomAnalogValueSleep.py | 148 +++++++++++ sandbox/add_remove_property.py | 59 +++++ tests/test_pdu/test_address.py | 175 +++++++++++-- .../test_character_string.py | 9 +- tests/test_primitive_data/test_enumerated.py | 42 +++- .../test_object_identifier.py | 10 +- tests/test_primitive_data/test_object_type.py | 22 +- 21 files changed, 741 insertions(+), 41 deletions(-) create mode 100755 samples/MultipleReadPropertyHammer.py create mode 100644 samples/RandomAnalogValueSleep.py create mode 100644 sandbox/add_remove_property.py diff --git a/py25/bacpypes/__init__.py b/py25/bacpypes/__init__.py index c8b909c..c391a4b 100755 --- a/py25/bacpypes/__init__.py +++ b/py25/bacpypes/__init__.py @@ -18,7 +18,7 @@ if _sys.platform not in _supported_platforms: # Project Metadata # -__version__ = '0.16.3' +__version__ = '0.16.4' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py25/bacpypes/app.py b/py25/bacpypes/app.py index 0dc4b46..059da41 100755 --- a/py25/bacpypes/app.py +++ b/py25/bacpypes/app.py @@ -135,8 +135,12 @@ class DeviceInfoCache: current_info = DeviceInfo() current_info.address = key current_info._cache_keys = (None, key) + current_info._ref_count = 1 self.cache[key] = current_info + else: + if _debug: DeviceInfoCache._debug(" - reference bump") + current_info._ref_count += 1 if _debug: DeviceInfoCache._debug(" - current_info: %r", current_info) @@ -177,11 +181,18 @@ class DeviceInfoCache: has finished with the device information.""" if _debug: DeviceInfoCache._debug("release_device_info %r", info) + # this information record might be used by more than one SSM + if info._ref_count > 1: + if _debug: DeviceInfoCache._debug(" - multiple references") + info._ref_count -= 1 + return + cache_id, cache_address = info._cache_keys if cache_id is not None: del self.cache[cache_id] if cache_address is not None: del self.cache[cache_address] + if _debug: DeviceInfoCache._debug(" - released") bacpypes_debugging(DeviceInfoCache) diff --git a/py25/bacpypes/constructeddata.py b/py25/bacpypes/constructeddata.py index 2e589b8..1d1cade 100755 --- a/py25/bacpypes/constructeddata.py +++ b/py25/bacpypes/constructeddata.py @@ -600,6 +600,11 @@ def ArrayOf(klass): # not found raise ValueError("%r not in array" % (value,)) + def remove(self, item): + # find the index of the item and delete it + indx = self.index(item) + self.__delitem__(indx) + def encode(self, taglist): if _debug: ArrayOf._debug("(%r)encode %r", self.__class__.__name__, taglist) diff --git a/py25/bacpypes/pdu.py b/py25/bacpypes/pdu.py index 4ed3df5..825309a 100755 --- a/py25/bacpypes/pdu.py +++ b/py25/bacpypes/pdu.py @@ -91,7 +91,7 @@ class Address: self.addrAddr = struct.pack('B', addr) self.addrLen = 1 - elif isinstance(addr, str): + elif isinstance(addr, basestring): if _debug: Address._debug(" - str") m = ip_address_mask_port_re.match(addr) @@ -263,7 +263,7 @@ class Address: addr, port = addr self.addrPort = int(port) - if isinstance(addr, str): + if isinstance(addr, basestring): if not addr: # when ('', n) is passed it is the local host address, but that # could be more than one on a multihomed machine, the empty string diff --git a/py25/bacpypes/primitivedata.py b/py25/bacpypes/primitivedata.py index 1335041..64070aa 100755 --- a/py25/bacpypes/primitivedata.py +++ b/py25/bacpypes/primitivedata.py @@ -1073,7 +1073,7 @@ class Enumerated(Atomic): # convert it to a string if you can self.value = self._xlate_table.get(arg, arg) - elif isinstance(arg, str): + elif isinstance(arg, basestring): if arg not in self._xlate_table: raise ValueError("undefined enumeration '%s'" % (arg,)) self.value = arg @@ -1088,7 +1088,7 @@ class Enumerated(Atomic): def get_long(self): if isinstance(self.value, (int, long)): return self.value - elif isinstance(self.value, str): + elif isinstance(self.value, basestring): return long(self._xlate_table[self.value]) else: raise TypeError("%s is an invalid enumeration value datatype" % (type(self.value),)) @@ -1132,7 +1132,7 @@ class Enumerated(Atomic): value = long(self.value) elif isinstance(self.value, long): value = self.value - elif isinstance(self.value, str): + elif isinstance(self.value, basestring): value = self._xlate_table[self.value] else: raise TypeError("%s is an invalid enumeration value datatype" % (type(self.value),)) @@ -1170,7 +1170,7 @@ class Enumerated(Atomic): value is wrong for the enumeration, the encoding will fail. """ return (isinstance(arg, (int, long)) and (arg >= 0)) or \ - isinstance(arg, str) + isinstance(arg, basestring) def __str__(self): return "%s(%s)" % (self.__class__.__name__, self.value) @@ -1607,7 +1607,7 @@ class ObjectIdentifier(Atomic): objType = self.objectTypeClass._xlate_table.get(objType, objType) elif isinstance(objType, long): objType = self.objectTypeClass._xlate_table.get(objType, int(objType)) - elif isinstance(objType, str): + elif isinstance(objType, basestring): # make sure the type is known if objType not in self.objectTypeClass._xlate_table: raise ValueError("unrecognized object type '%s'" % (objType,)) @@ -1629,7 +1629,7 @@ class ObjectIdentifier(Atomic): pass elif isinstance(objType, long): objType = int(objType) - elif isinstance(objType, str): + elif isinstance(objType, basestring): # turn it back into an integer objType = self.objectTypeClass()[objType] else: @@ -1680,7 +1680,7 @@ class ObjectIdentifier(Atomic): # rip it apart objType, objInstance = self.value - if isinstance(objType, str): + if isinstance(objType, basestring): typestr = objType elif objType < 0: typestr = "Bad %d" % (objType,) diff --git a/py27/bacpypes/__init__.py b/py27/bacpypes/__init__.py index c8b909c..c391a4b 100755 --- a/py27/bacpypes/__init__.py +++ b/py27/bacpypes/__init__.py @@ -18,7 +18,7 @@ if _sys.platform not in _supported_platforms: # Project Metadata # -__version__ = '0.16.3' +__version__ = '0.16.4' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py27/bacpypes/app.py b/py27/bacpypes/app.py index bd35a64..3aa33ec 100755 --- a/py27/bacpypes/app.py +++ b/py27/bacpypes/app.py @@ -135,8 +135,12 @@ class DeviceInfoCache: current_info = DeviceInfo() current_info.address = key current_info._cache_keys = (None, key) + current_info._ref_count = 1 self.cache[key] = current_info + else: + if _debug: DeviceInfoCache._debug(" - reference bump") + current_info._ref_count += 1 if _debug: DeviceInfoCache._debug(" - current_info: %r", current_info) @@ -177,11 +181,18 @@ class DeviceInfoCache: has finished with the device information.""" if _debug: DeviceInfoCache._debug("release_device_info %r", info) + # this information record might be used by more than one SSM + if info._ref_count > 1: + if _debug: DeviceInfoCache._debug(" - multiple references") + info._ref_count -= 1 + return + cache_id, cache_address = info._cache_keys if cache_id is not None: del self.cache[cache_id] if cache_address is not None: del self.cache[cache_address] + if _debug: DeviceInfoCache._debug(" - released") # # Application diff --git a/py27/bacpypes/constructeddata.py b/py27/bacpypes/constructeddata.py index 75818ad..38e491c 100755 --- a/py27/bacpypes/constructeddata.py +++ b/py27/bacpypes/constructeddata.py @@ -598,6 +598,11 @@ def ArrayOf(klass): # not found raise ValueError("%r not in array" % (value,)) + def remove(self, item): + # find the index of the item and delete it + indx = self.index(item) + self.__delitem__(indx) + def encode(self, taglist): if _debug: ArrayOf._debug("(%r)encode %r", self.__class__.__name__, taglist) diff --git a/py27/bacpypes/pdu.py b/py27/bacpypes/pdu.py index 5505361..eb848bd 100755 --- a/py27/bacpypes/pdu.py +++ b/py27/bacpypes/pdu.py @@ -93,7 +93,7 @@ class Address: self.addrAddr = struct.pack('B', addr) self.addrLen = 1 - elif isinstance(addr, str): + elif isinstance(addr, basestring): if _debug: Address._debug(" - str") m = ip_address_mask_port_re.match(addr) @@ -259,7 +259,7 @@ class Address: addr, port = addr self.addrPort = int(port) - if isinstance(addr, str): + if isinstance(addr, basestring): if not addr: # when ('', n) is passed it is the local host address, but that # could be more than one on a multihomed machine, the empty string diff --git a/py27/bacpypes/primitivedata.py b/py27/bacpypes/primitivedata.py index 1047f43..91c10d7 100755 --- a/py27/bacpypes/primitivedata.py +++ b/py27/bacpypes/primitivedata.py @@ -1078,7 +1078,7 @@ class Enumerated(Atomic): # convert it to a string if you can self.value = self._xlate_table.get(arg, arg) - elif isinstance(arg, str): + elif isinstance(arg, basestring): if arg not in self._xlate_table: raise ValueError("undefined enumeration '%s'" % (arg,)) self.value = arg @@ -1093,7 +1093,7 @@ class Enumerated(Atomic): def get_long(self): if isinstance(self.value, (int, long)): return self.value - elif isinstance(self.value, str): + elif isinstance(self.value, basestring): return long(self._xlate_table[self.value]) else: raise TypeError("%s is an invalid enumeration value datatype" % (type(self.value),)) @@ -1137,7 +1137,7 @@ class Enumerated(Atomic): value = long(self.value) elif isinstance(self.value, long): value = self.value - elif isinstance(self.value, str): + elif isinstance(self.value, basestring): value = self._xlate_table[self.value] else: raise TypeError("%s is an invalid enumeration value datatype" % (type(self.value),)) @@ -1613,7 +1613,7 @@ class ObjectIdentifier(Atomic): objType = self.objectTypeClass._xlate_table.get(objType, objType) elif isinstance(objType, long): objType = self.objectTypeClass._xlate_table.get(objType, int(objType)) - elif isinstance(objType, str): + elif isinstance(objType, basestring): # make sure the type is known if objType not in self.objectTypeClass._xlate_table: raise ValueError("unrecognized object type '%s'" % (objType,)) @@ -1635,7 +1635,7 @@ class ObjectIdentifier(Atomic): pass elif isinstance(objType, long): objType = int(objType) - elif isinstance(objType, str): + elif isinstance(objType, basestring): # turn it back into an integer objType = self.objectTypeClass()[objType] else: @@ -1686,7 +1686,7 @@ class ObjectIdentifier(Atomic): # rip it apart objType, objInstance = self.value - if isinstance(objType, str): + if isinstance(objType, basestring): typestr = objType elif objType < 0: typestr = "Bad %d" % (objType,) diff --git a/py34/bacpypes/__init__.py b/py34/bacpypes/__init__.py index ae4133b..6692b8e 100755 --- a/py34/bacpypes/__init__.py +++ b/py34/bacpypes/__init__.py @@ -18,7 +18,7 @@ if _sys.platform not in _supported_platforms: # Project Metadata # -__version__ = '0.16.3' +__version__ = '0.16.4' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py34/bacpypes/app.py b/py34/bacpypes/app.py index bd35a64..3aa33ec 100755 --- a/py34/bacpypes/app.py +++ b/py34/bacpypes/app.py @@ -135,8 +135,12 @@ class DeviceInfoCache: current_info = DeviceInfo() current_info.address = key current_info._cache_keys = (None, key) + current_info._ref_count = 1 self.cache[key] = current_info + else: + if _debug: DeviceInfoCache._debug(" - reference bump") + current_info._ref_count += 1 if _debug: DeviceInfoCache._debug(" - current_info: %r", current_info) @@ -177,11 +181,18 @@ class DeviceInfoCache: has finished with the device information.""" if _debug: DeviceInfoCache._debug("release_device_info %r", info) + # this information record might be used by more than one SSM + if info._ref_count > 1: + if _debug: DeviceInfoCache._debug(" - multiple references") + info._ref_count -= 1 + return + cache_id, cache_address = info._cache_keys if cache_id is not None: del self.cache[cache_id] if cache_address is not None: del self.cache[cache_address] + if _debug: DeviceInfoCache._debug(" - released") # # Application diff --git a/py34/bacpypes/constructeddata.py b/py34/bacpypes/constructeddata.py index 45dc228..54b9f5a 100755 --- a/py34/bacpypes/constructeddata.py +++ b/py34/bacpypes/constructeddata.py @@ -598,6 +598,11 @@ def ArrayOf(klass): # not found raise ValueError("%r not in array" % (value,)) + def remove(self, item): + # find the index of the item and delete it + indx = self.index(item) + self.__delitem__(indx) + def encode(self, taglist): if _debug: ArrayOf._debug("(%r)encode %r", self.__class__.__name__, taglist) diff --git a/samples/MultipleReadPropertyHammer.py b/samples/MultipleReadPropertyHammer.py new file mode 100755 index 0000000..7bfa4e8 --- /dev/null +++ b/samples/MultipleReadPropertyHammer.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python + +""" +Mutliple Read Property Hammer + +This application blasts a list of ReadPropertyRequest messages with no +regard to the number of simultaneous requests to the same device. The +ReadPointListApplication is constructed like the BIPSimpleApplication but +without the ApplicationIOController interface and sieve. +""" + +import os +from time import time as _time +from copy import copy as _copy + +from random import shuffle + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run +from bacpypes.comm import bind +from bacpypes.task import RecurringTask + +from bacpypes.pdu import Address + +from bacpypes.app import Application +from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint +from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement +from bacpypes.bvllservice import BIPSimple, AnnexJCodec, UDPMultiplexer + +from bacpypes.apdu import ReadPropertyRequest + +from bacpypes.service.device import LocalDeviceObject, WhoIsIAmServices +from bacpypes.service.object import ReadWritePropertyServices + + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +args = None +this_application = None + +# settings +INTERVAL = float(os.getenv('INTERVAL', 10.0)) + +# point list, set according to your device +point_list = [ + ('10.0.1.21:47809', 'analogValue', 1, 'presentValue'), + ('10.0.1.21:47809', 'analogValue', 2, 'presentValue'), + ('10.0.1.21:47809', 'analogValue', 3, 'presentValue'), + ('10.0.1.21:47809', 'analogValue', 4, 'presentValue'), + ('10.0.1.21:47809', 'analogValue', 5, 'presentValue'), + ] + +# +# ReadPointListApplication +# + +@bacpypes_debugging +class ReadPointListApplication(Application, WhoIsIAmServices, ReadWritePropertyServices, RecurringTask): + + def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None): + if _debug: ReadPointListApplication._debug("__init__ %r %r deviceInfoCache=%r aseID=%r", localDevice, localAddress, deviceInfoCache, aseID) + global args + + Application.__init__(self, localDevice, deviceInfoCache, aseID=aseID) + RecurringTask.__init__(self, args.interval * 1000) + + # 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 = NetworkServiceElement() + 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) + + # install the task + self.install_task() + + # timer + self.start_time = None + + # pending requests + self.pending_requests = {} + + def process_task(self): + if _debug: ReadPointListApplication._debug("process_task") + global point_list + + # we might not have finished from the last round + if self.pending_requests: + if _debug: ReadPointListApplication._debug(" - %d pending", len(self.pending_requests)) + return + + # start the clock + self.start_time = _time() + + # make a copy of the point list and shuffle it + point_list_copy = _copy(point_list) + shuffle(point_list_copy) + + # loop through the points + for addr, obj_type, obj_inst, prop_id in point_list_copy: + # build a request + request = ReadPropertyRequest( + objectIdentifier=(obj_type, obj_inst), + propertyIdentifier=prop_id, + ) + request.pduDestination = Address(addr) + if _debug: ReadPointListApplication._debug(" - request: %r", request) + + # send the request + self.request(request) + + # get the destination address from the pdu + request_key = request.pduDestination, request.apduInvokeID + if _debug: ReadPointListApplication._debug(" - request_key: %r", request_key) + + # make sure it's unused + if request_key in self.pending_requests: + raise RuntimeError("request key already used: %r" % (request_key,)) + + # add this to pending requests + self.pending_requests[request_key] = request + + def confirmation(self, apdu): + if _debug: ReadPointListApplication._debug("confirmation %r", apdu) + + # get the source address from the pdu + request_key = apdu.pduSource, apdu.apduInvokeID + if _debug: ReadPointListApplication._debug(" - request_key: %r", request_key) + + # make sure it's unused + if request_key not in self.pending_requests: + raise RuntimeError("request missing: %r" % (request_key,)) + + # this is no longer pending + del self.pending_requests[request_key] + + # we could be done with this interval + if not self.pending_requests: + elapsed_time = _time() - self.start_time + if _debug: ReadPointListApplication._debug(" - completed interval, %r seconds", elapsed_time) + + +# +# __main__ +# + +def main(): + global args, this_application + + # parse the command line arguments + parser = ConfigArgumentParser(description=__doc__) + + # add an option to override the interval time + parser.add_argument('--interval', type=float, + help="amount of time between intervals", + default=INTERVAL, + ) + + # parse the command line arguments + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # make a device object + this_device = LocalDeviceObject( + objectName=args.ini.objectname, + objectIdentifier=int(args.ini.objectidentifier), + maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), + segmentationSupported=args.ini.segmentationsupported, + vendorIdentifier=int(args.ini.vendoridentifier), + ) + + # make a simple application + this_application = ReadPointListApplication(this_device, args.ini.address) + + # get the services supported + services_supported = this_application.get_services_supported() + if _debug: _log.debug(" - services_supported: %r", services_supported) + + # let the device object know + this_device.protocolServicesSupported = services_supported.value + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/RandomAnalogValueSleep.py b/samples/RandomAnalogValueSleep.py new file mode 100644 index 0000000..ea71cdb --- /dev/null +++ b/samples/RandomAnalogValueSleep.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python + +""" +Random Value Property with Sleep + +This application is a server of analog value objects that return a random +number when the present value is read. This version has an additional +'sleep' time that slows down its performance. +""" + +import os +import random +import time + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run + +from bacpypes.primitivedata import Real +from bacpypes.object import AnalogValueObject, Property, register_object_type +from bacpypes.errors import ExecutionError + +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# settings +SLEEP_TIME = float(os.getenv('SLEEP_TIME', 0.1)) +RANDOM_OBJECT_COUNT = int(os.getenv('RANDOM_OBJECT_COUNT', 10)) + +# globals +args = None + +# +# RandomValueProperty +# + +class RandomValueProperty(Property): + + def __init__(self, identifier): + if _debug: RandomValueProperty._debug("__init__ %r", identifier) + Property.__init__(self, identifier, Real, default=0.0, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: RandomValueProperty._debug("ReadProperty %r arrayIndex=%r", obj, arrayIndex) + global args + + # access an array + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # sleep a little + time.sleep(args.sleep) + + # return a random value + value = random.random() * 100.0 + if _debug: RandomValueProperty._debug(" - value: %r", value) + + return value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + if _debug: RandomValueProperty._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", obj, value, arrayIndex, priority, direct) + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +bacpypes_debugging(RandomValueProperty) + +# +# Random Value Object Type +# + +class RandomAnalogValueObject(AnalogValueObject): + + properties = [ + RandomValueProperty('presentValue'), + ] + + def __init__(self, **kwargs): + if _debug: RandomAnalogValueObject._debug("__init__ %r", kwargs) + AnalogValueObject.__init__(self, **kwargs) + +bacpypes_debugging(RandomAnalogValueObject) +register_object_type(RandomAnalogValueObject) + +# +# __main__ +# + +def main(): + global args + + # parse the command line arguments + parser = ConfigArgumentParser(description=__doc__) + + # add an option to override the sleep time + parser.add_argument('--sleep', type=float, + help="sleep before returning the value", + default=SLEEP_TIME, + ) + + # parse the command line arguments + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # make a device object + this_device = LocalDeviceObject( + objectName=args.ini.objectname, + objectIdentifier=('device', int(args.ini.objectidentifier)), + maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), + segmentationSupported=args.ini.segmentationsupported, + vendorIdentifier=int(args.ini.vendoridentifier), + ) + + # make a sample application + this_application = BIPSimpleApplication(this_device, args.ini.address) + + # get the services supported + services_supported = this_application.get_services_supported() + if _debug: _log.debug(" - services_supported: %r", services_supported) + + # let the device object know + this_device.protocolServicesSupported = services_supported.value + + # make some random input objects + for i in range(1, RANDOM_OBJECT_COUNT+1): + ravo = RandomAnalogValueObject( + objectIdentifier=('analogValue', i), + objectName='Random-%d' % (i,), + ) + _log.debug(" - ravo: %r", ravo) + this_application.add_object(ravo) + + # make sure they are all there + _log.debug(" - object list: %r", this_device.objectList) + + _log.debug("running") + + run() + + _log.debug("fini") + +if __name__ == "__main__": + main() diff --git a/sandbox/add_remove_property.py b/sandbox/add_remove_property.py new file mode 100644 index 0000000..e5a90be --- /dev/null +++ b/sandbox/add_remove_property.py @@ -0,0 +1,59 @@ + +from bacpypes.basetypes import PropertyIdentifier +from bacpypes.constructeddata import ArrayOf +from bacpypes.object import AnalogValueObject + +# create an array of property identifiers datatype +ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) + +aopi = ArrayOfPropertyIdentifier() +aopi.append('objectName') +aopi.append('objectType') +aopi.append('description') +aopi.debug_contents() + +aopi.remove('objectType') +aopi.debug_contents() + +print("Create an Analog Value Object") +av = AnalogValueObject( + objectName='av-sample', + objectIdentifier=('analogValue', 1), + description="sample", + ) +av.debug_contents() +print("") + +print("Change the description") +av.description = "something else" +av.debug_contents() +print("") + + +# get the description property by the attribute name +description_property = av._attr_to_property('description') +print("description_property = %r" % (description_property,)) +print("") + +print("Delete the property") +av.delete_property(description_property) +print("...property deleted") + +try: + av.description = "this raises an exception" +except Exception as err: + print(repr(err)) +av.debug_contents() +print("") + +print("===== Add the property") +av.add_property(description_property) +print("...property added") + +try: + av.description = "this works" +except Exception as err: + print(repr(err)) +av.debug_contents() +print("") + diff --git a/tests/test_pdu/test_address.py b/tests/test_pdu/test_address.py index af38297..682a24e 100644 --- a/tests/test_pdu/test_address.py +++ b/tests/test_pdu/test_address.py @@ -74,8 +74,8 @@ class TestAddress(unittest.TestCase, MatchAddressMixin): with self.assertRaises(ValueError): Address(256) - def test_address_ipv4(self): - if _debug: TestAddress._debug("test_address_ipv4") + def test_address_ipv4_str(self): + if _debug: TestAddress._debug("test_address_ipv4_str") # test IPv4 local station address test_addr = Address("1.2.3.4") @@ -92,16 +92,42 @@ class TestAddress(unittest.TestCase, MatchAddressMixin): self.match_address(test_addr, 2, None, 6, '01020304bb7f') assert str(test_addr) == "0x01020304bb7f" - def test_address_eth(self): - if _debug: TestAddress._debug("test_address_eth") + def test_address_ipv4_unicode(self): + if _debug: TestAddress._debug("test_address_ipv4_unicode") + + # test IPv4 local station address + test_addr = Address(u"1.2.3.4") + self.match_address(test_addr, 2, None, 6, '01020304BAC0') + assert str(test_addr) == u"1.2.3.4" + + # test IPv4 local station address with non-standard port + test_addr = Address(u"1.2.3.4:47809") + self.match_address(test_addr, 2, None, 6, '01020304BAC1') + assert str(test_addr) == u"1.2.3.4:47809" + + # test IPv4 local station address with unrecognized port + test_addr = Address(u"1.2.3.4:47999") + self.match_address(test_addr, 2, None, 6, '01020304bb7f') + assert str(test_addr) == u"0x01020304bb7f" + + def test_address_eth_str(self): + if _debug: TestAddress._debug("test_address_eth_str") # test Ethernet local station address test_addr = Address("01:02:03:04:05:06") self.match_address(test_addr, 2, None, 6, '010203040506') assert str(test_addr) == "0x010203040506" - def test_address_local_station(self): - if _debug: TestAddress._debug("test_address_local_station") + def test_address_eth_unicode(self): + if _debug: TestAddress._debug("test_address_eth_unicode") + + # test Ethernet local station address + test_addr = Address(u"01:02:03:04:05:06") + self.match_address(test_addr, 2, None, 6, '010203040506') + assert str(test_addr) == u"0x010203040506" + + def test_address_local_station_str(self): + if _debug: TestAddress._debug("test_address_local_station_str") # test integer local station test_addr = Address("1") @@ -134,16 +160,58 @@ class TestAddress(unittest.TestCase, MatchAddressMixin): self.match_address(test_addr, 2, None, 2, '0102') assert str(test_addr) == "0x0102" - def test_address_local_broadcast(self): - if _debug: TestAddress._debug("test_address_local_broadcast") + def test_address_local_station_unicode(self): + if _debug: TestAddress._debug("test_address_local_station_unicode") + + # test integer local station + test_addr = Address(u"1") + self.match_address(test_addr, 2, None, 1, '01') + assert str(test_addr) == u"1" + + test_addr = Address(u"254") + self.match_address(test_addr, 2, None, 1, 'fe') + assert str(test_addr) == u"254" + + # test bad integer string + with self.assertRaises(ValueError): + Address("256") + + # test modern hex string + test_addr = Address(u"0x01") + self.match_address(test_addr, 2, None, 1, '01') + assert str(test_addr) == u"1" + + test_addr = Address(u"0x0102") + self.match_address(test_addr, 2, None, 2, '0102') + assert str(test_addr) == u"0x0102" + + # test old school hex string + test_addr = Address(u"X'01'") + self.match_address(test_addr, 2, None, 1, '01') + assert str(test_addr) == u"1" + + test_addr = Address(u"X'0102'") + self.match_address(test_addr, 2, None, 2, '0102') + assert str(test_addr) == u"0x0102" + + def test_address_local_broadcast_str(self): + if _debug: TestAddress._debug("test_address_local_broadcast_str") # test local broadcast test_addr = Address("*") self.match_address(test_addr, 1, None, None, None) assert str(test_addr) == "*" - def test_address_remote_broadcast(self): - if _debug: TestAddress._debug("test_address_remote_broadcast") + def test_address_local_broadcast_unicode(self): + if _debug: TestAddress._debug("test_address_local_broadcast_unicode") + + # test local broadcast + test_addr = Address(u"*") + self.match_address(test_addr, 1, None, None, None) + assert str(test_addr) == u"*" + + def test_address_remote_broadcast_str(self): + if _debug: TestAddress._debug("test_address_remote_broadcast_str") # test remote broadcast test_addr = Address("1:*") @@ -154,8 +222,20 @@ class TestAddress(unittest.TestCase, MatchAddressMixin): with self.assertRaises(ValueError): Address("65536:*") - def test_address_remote_station(self): - if _debug: TestAddress._debug("test_address_remote_station") + def test_address_remote_broadcast_unicode(self): + if _debug: TestAddress._debug("test_address_remote_broadcast_unicode") + + # test remote broadcast + test_addr = Address(u"1:*") + self.match_address(test_addr, 3, 1, None, None) + assert str(test_addr) == u"1:*" + + # test remote broadcast bad network + with self.assertRaises(ValueError): + Address("65536:*") + + def test_address_remote_station_str(self): + if _debug: TestAddress._debug("test_address_remote_station_str") # test integer remote station test_addr = Address("1:2") @@ -198,14 +278,66 @@ class TestAddress(unittest.TestCase, MatchAddressMixin): with self.assertRaises(ValueError): Address("65536:X'02'") - def test_address_global_broadcast(self): - if _debug: TestAddress._debug("test_address_global_broadcast") + def test_address_remote_station_unicode(self): + if _debug: TestAddress._debug("test_address_remote_station_unicode") + + # test integer remote station + test_addr = Address(u"1:2") + self.match_address(test_addr, 4, 1, 1, '02') + assert str(test_addr) == u"1:2" + + test_addr = Address(u"1:254") + self.match_address(test_addr, 4, 1, 1, 'fe') + assert str(test_addr) == u"1:254" + + # test bad network and node + with self.assertRaises(ValueError): + Address(u"65536:2") + with self.assertRaises(ValueError): + Address(u"1:256") + + # test modern hex string + test_addr = Address(u"1:0x02") + self.match_address(test_addr, 4, 1, 1, '02') + assert str(test_addr) == u"1:2" + + # test bad network + with self.assertRaises(ValueError): + Address(u"65536:0x02") + + test_addr = Address(u"1:0x0203") + self.match_address(test_addr, 4, 1, 2, '0203') + assert str(test_addr) == u"1:0x0203" + + # test old school hex string + test_addr = Address(u"1:X'02'") + self.match_address(test_addr, 4, 1, 1, '02') + assert str(test_addr) == u"1:2" + + test_addr = Address(u"1:X'0203'") + self.match_address(test_addr, 4, 1, 2, '0203') + assert str(test_addr) == u"1:0x0203" + + # test bad network + with self.assertRaises(ValueError): + Address(u"65536:X'02'") + + def test_address_global_broadcast_str(self): + if _debug: TestAddress._debug("test_address_global_broadcast_str") # test local broadcast test_addr = Address("*:*") self.match_address(test_addr, 5, None, None, None) assert str(test_addr) == "*:*" + def test_address_global_broadcast_unicode(self): + if _debug: TestAddress._debug("test_address_global_broadcast_unicode") + + # test local broadcast + test_addr = Address(u"*:*") + self.match_address(test_addr, 5, None, None, None) + assert str(test_addr) == u"*:*" + @bacpypes_debugging class TestLocalStation(unittest.TestCase, MatchAddressMixin): @@ -368,8 +500,8 @@ class TestGlobalBroadcast(unittest.TestCase, MatchAddressMixin): @bacpypes_debugging class TestAddressEquality(unittest.TestCase, MatchAddressMixin): - def test_address_equality(self): - if _debug: TestAddressEquality._debug("test_address_equality") + def test_address_equality_str(self): + if _debug: TestAddressEquality._debug("test_address_equality_str") assert Address(1) == LocalStation(1) assert Address("2") == LocalStation(2) @@ -377,3 +509,14 @@ class TestAddressEquality(unittest.TestCase, MatchAddressMixin): assert Address("3:4") == RemoteStation(3, 4) assert Address("5:*") == RemoteBroadcast(5) assert Address("*:*") == GlobalBroadcast() + + def test_address_equality_unicode(self): + if _debug: TestAddressEquality._debug("test_address_equality_unicode") + + assert Address(1) == LocalStation(1) + assert Address(u"2") == LocalStation(2) + assert Address(u"*") == LocalBroadcast() + assert Address(u"3:4") == RemoteStation(3, 4) + assert Address(u"5:*") == RemoteBroadcast(5) + assert Address(u"*:*") == GlobalBroadcast() + diff --git a/tests/test_primitive_data/test_character_string.py b/tests/test_primitive_data/test_character_string.py index 15f66ec..1043056 100644 --- a/tests/test_primitive_data/test_character_string.py +++ b/tests/test_primitive_data/test_character_string.py @@ -93,6 +93,13 @@ class TestCharacterString(unittest.TestCase): assert obj.value == "hello" assert str(obj) == "CharacterString(0,X'68656c6c6f')" + def test_character_string_unicode(self): + if _debug: TestCharacterString._debug("test_character_string_unicode") + + obj = CharacterString(u"hello") + assert obj.value == u"hello" + assert str(obj) == "CharacterString(0,X'68656c6c6f')" + def test_character_string_tag(self): if _debug: TestCharacterString._debug("test_character_string_tag") @@ -126,4 +133,4 @@ class TestCharacterString(unittest.TestCase): obj = CharacterString(character_string_tag('')) character_string_endec("", '00') - character_string_endec("abc", '00616263') \ No newline at end of file + character_string_endec("abc", '00616263') diff --git a/tests/test_primitive_data/test_enumerated.py b/tests/test_primitive_data/test_enumerated.py index 67d823c..31fe4c1 100644 --- a/tests/test_primitive_data/test_enumerated.py +++ b/tests/test_primitive_data/test_enumerated.py @@ -18,6 +18,14 @@ _debug = 0 _log = ModuleLogger(globals()) +class QuickBrownFox(Enumerated): + enumerations = { + 'quick': 0, + 'brown': 1, + 'fox': 2, + } + + @bacpypes_debugging def enumerated_tag(x): """Convert a hex string to an enumerated application tag.""" @@ -93,6 +101,38 @@ class TestEnumerated(unittest.TestCase): with self.assertRaises(ValueError): Enumerated(-1) + def test_enumerated_str(self): + if _debug: TestEnumerated._debug("test_enumerated_str") + + obj = QuickBrownFox('quick') + assert obj.value == 'quick' + assert str(obj) == "QuickBrownFox(quick)" + + with self.assertRaises(ValueError): + QuickBrownFox(-1) + with self.assertRaises(ValueError): + QuickBrownFox('lazyDog') + + tag = Tag(Tag.applicationTagClass, Tag.enumeratedAppTag, 1, xtob('01')) + obj = QuickBrownFox(tag) + assert obj.value == 'brown' + + def test_enumerated_unicode(self): + if _debug: TestEnumerated._debug("test_enumerated_unicode") + + obj = QuickBrownFox(u'quick') + assert obj.value == u'quick' + assert str(obj) == "QuickBrownFox(quick)" + + with self.assertRaises(ValueError): + QuickBrownFox(-1) + with self.assertRaises(ValueError): + QuickBrownFox(u'lazyDog') + + tag = Tag(Tag.applicationTagClass, Tag.enumeratedAppTag, 1, xtob('01')) + obj = QuickBrownFox(tag) + assert obj.value == u'brown' + def test_enumerated_tag(self): if _debug: TestEnumerated._debug("test_enumerated_tag") @@ -138,4 +178,4 @@ class TestEnumerated(unittest.TestCase): enumerated_endec(8388608, '800000') enumerated_endec(2147483647, '7fffffff') - enumerated_endec(2147483648, '80000000') \ No newline at end of file + enumerated_endec(2147483648, '80000000') diff --git a/tests/test_primitive_data/test_object_identifier.py b/tests/test_primitive_data/test_object_identifier.py index d14f161..c97a144 100644 --- a/tests/test_primitive_data/test_object_identifier.py +++ b/tests/test_primitive_data/test_object_identifier.py @@ -97,6 +97,12 @@ class TestObjectIdentifier(unittest.TestCase): def test_object_identifier_tuple(self): if _debug: TestObjectIdentifier._debug("test_object_identifier_tuple") + obj = ObjectIdentifier(('analogInput', 0)) + assert obj.value == ('analogInput', 0) + + obj = ObjectIdentifier((u'analogInput', 0)) + assert obj.value == (u'analogInput', 0) + with self.assertRaises(ValueError): ObjectIdentifier((0, -1)) with self.assertRaises(ValueError): @@ -136,5 +142,7 @@ class TestObjectIdentifier(unittest.TestCase): # test standard types object_identifier_endec(('analogInput', 0), '00000000') + object_identifier_endec((u'analogInput', 0), '00000000') + + # test vendor types - # test vendor types \ No newline at end of file diff --git a/tests/test_primitive_data/test_object_type.py b/tests/test_primitive_data/test_object_type.py index 62b8eb0..dc6e470 100644 --- a/tests/test_primitive_data/test_object_type.py +++ b/tests/test_primitive_data/test_object_type.py @@ -23,7 +23,7 @@ class MyObjectType(ObjectType): 'myAnalogInput': 128, 'myAnalogOutput': 129, 'myAnalogValue': 130, - } + } expand_enumerations(MyObjectType) @@ -113,6 +113,13 @@ class TestObjectType(unittest.TestCase): obj = ObjectType('analogInput') assert obj.value == 'analogInput' + def test_object_type_unicode(self): + if _debug: TestObjectType._debug("test_object_type_unicode") + + # known strings are accepted + obj = ObjectType(u'analogInput') + assert obj.value == u'analogInput' + def test_extended_object_type_int(self): if _debug: TestObjectType._debug("test_extended_object_type_int") @@ -137,6 +144,17 @@ class TestObjectType(unittest.TestCase): with self.assertRaises(ValueError): MyObjectType('snork') + def test_extended_object_type_unicode(self): + if _debug: TestObjectType._debug("test_extended_object_type_unicode") + + # known strings are accepted + obj = MyObjectType(u'myAnalogInput') + assert obj.value == u'myAnalogInput' + + # unknown strings are rejected + with self.assertRaises(ValueError): + MyObjectType(u'snork') + def test_object_type_tag(self): if _debug: TestObjectType._debug("test_object_type_tag") @@ -174,4 +192,4 @@ class TestObjectType(unittest.TestCase): object_type_endec('analogOutput', '01') object_type_endec(127, '7f') - object_type_endec(128, '80') \ No newline at end of file + object_type_endec(128, '80') From ab8a8b8aa482c4bf4e3123ed0c46170a51c5e1ca Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 28 Nov 2017 20:40:36 -0500 Subject: [PATCH 2/3] doc: Fix typoes and omissions in getting started doc. (#150) * doc: Fix typoes and omissions in getting started doc. * doc: Another typo Thank you! --- doc/source/gettingstarted/gettingstarted001.rst | 4 ++-- doc/source/gettingstarted/gettingstarted002.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/gettingstarted/gettingstarted001.rst b/doc/source/gettingstarted/gettingstarted001.rst index ed449ef..c88263a 100644 --- a/doc/source/gettingstarted/gettingstarted001.rst +++ b/doc/source/gettingstarted/gettingstarted001.rst @@ -152,7 +152,7 @@ of the sample configuration file, and edit it for your site:: communicate as peers, so it is not unusual for an application to act as both a client and a server at the same time. -A typical BACpypes.ini file contains +A typical BACpypes.ini file contains:: [BACpypes] objectName: Betelgeuse @@ -172,7 +172,7 @@ UDP Communications Issues BACnet devices communicate using UDP rather than TCP. This is so devices do not need to implement a full IP stack (although -many of them do becuase they support multiple protocols, including +many of them do because they support multiple protocols, including having embedded web servers). There are two types of UDP messages; *unicast* which is a message diff --git a/doc/source/gettingstarted/gettingstarted002.rst b/doc/source/gettingstarted/gettingstarted002.rst index 5af52c9..4d22180 100644 --- a/doc/source/gettingstarted/gettingstarted002.rst +++ b/doc/source/gettingstarted/gettingstarted002.rst @@ -125,7 +125,7 @@ be specified, so this limits the output to one hundred files:: .. caution:: - The traffice.txt file will be saved in the local directory (pwd) + The traffic.txt file will be saved in the local directory (pwd) The definition of debug:: From 89a9ac41f97ea546e5c34817216e07cee0b195d6 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Wed, 6 Dec 2017 21:25:28 -0500 Subject: [PATCH 3/3] release 0.16.5 good to go --- py25/bacpypes/__init__.py | 2 +- py25/bacpypes/basetypes.py | 22 +- py25/bacpypes/bvllservice.py | 1 + py25/bacpypes/constructeddata.py | 8 +- py25/bacpypes/iocb.py | 3 +- py25/bacpypes/object.py | 81 ++++-- py25/bacpypes/service/device.py | 35 +-- py25/bacpypes/service/object.py | 60 +++- py25/bacpypes/udp.py | 2 + py27/bacpypes/__init__.py | 2 +- py27/bacpypes/basetypes.py | 23 +- py27/bacpypes/bvllservice.py | 1 + py27/bacpypes/constructeddata.py | 8 +- py27/bacpypes/iocb.py | 5 +- py27/bacpypes/object.py | 82 ++++-- py27/bacpypes/service/device.py | 37 +-- py27/bacpypes/service/object.py | 60 +++- py27/bacpypes/udp.py | 2 + py34/bacpypes/__init__.py | 2 +- py34/bacpypes/basetypes.py | 22 +- py34/bacpypes/bvllservice.py | 1 + py34/bacpypes/constructeddata.py | 10 +- py34/bacpypes/object.py | 81 ++++-- py34/bacpypes/service/device.py | 35 +-- py34/bacpypes/service/object.py | 60 +++- py34/bacpypes/udp.py | 2 + samples/ReadPropertyMultipleServer.py | 7 +- samples/ReadWriteEventMessageTexts.py | 265 ++++++++++++++++++ tests/__init__.py | 1 + tests/test_constructed_data/__init__.py | 14 + tests/test_constructed_data/helpers.py | 88 ++++++ tests/test_constructed_data/test_any.py | 4 + .../test_constructed_data/test_any_atomic.py | 4 + tests/test_constructed_data/test_array_of.py | 198 +++++++++++++ tests/test_constructed_data/test_choice.py | 4 + tests/test_constructed_data/test_sequence.py | 204 ++++++++++++++ .../test_constructed_data/test_sequence_of.py | 4 + tests/test_service/test_object.py | 257 ++++++++++++++++- 38 files changed, 1524 insertions(+), 173 deletions(-) create mode 100644 samples/ReadWriteEventMessageTexts.py create mode 100644 tests/test_constructed_data/__init__.py create mode 100644 tests/test_constructed_data/helpers.py create mode 100644 tests/test_constructed_data/test_any.py create mode 100644 tests/test_constructed_data/test_any_atomic.py create mode 100644 tests/test_constructed_data/test_array_of.py create mode 100644 tests/test_constructed_data/test_choice.py create mode 100644 tests/test_constructed_data/test_sequence.py create mode 100644 tests/test_constructed_data/test_sequence_of.py diff --git a/py25/bacpypes/__init__.py b/py25/bacpypes/__init__.py index c391a4b..f72f8b7 100755 --- a/py25/bacpypes/__init__.py +++ b/py25/bacpypes/__init__.py @@ -18,7 +18,7 @@ if _sys.platform not in _supported_platforms: # Project Metadata # -__version__ = '0.16.4' +__version__ = '0.16.5' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py25/bacpypes/basetypes.py b/py25/bacpypes/basetypes.py index 5caa303..b5485f9 100755 --- a/py25/bacpypes/basetypes.py +++ b/py25/bacpypes/basetypes.py @@ -898,6 +898,7 @@ class EventType(Enumerated): , 'unsignedOutOfRange':16 , 'changeOfCharacterstring':17 , 'changeOfStatusFlags':18 + , 'changeOfReliability':19 } class FaultType(Enumerated): @@ -1471,7 +1472,18 @@ class Reliability(Enumerated): , 'multiStateFault':9 , 'configurationError':10 , 'communicationFailure':12 - , 'numberFault':13 + , 'memberFault': 13 + , 'monitoredObjectFault': 14 + , 'tripped': 15 + , 'lampFailure': 16 + , 'activationFailure': 17 + , 'renewDHCPFailure': 18 + , 'renewFDRegistration-failure': 19 + , 'restartAutoNegotiationFailure': 20 + , 'restartFailure': 21 + , 'proprietaryCommandFailure': 22 + , 'faultsListed': 23 + , 'referencedObjectFault': 24 } class RestartReason(Enumerated): @@ -2261,6 +2273,13 @@ class NotificationParametersChangeOfStatusFlagsType(Sequence): , Element('referencedFlags', StatusFlags, 1) ] +class NotificationParametersChangeOfReliabilityType(Sequence): + sequenceElements = \ + [ Element('reliability', Reliability, 0) + , Element('statusFlags', StatusFlags, 1) + , Element('propertyValues', SequenceOf(PropertyValue), 2) + ] + class NotificationParameters(Choice): choiceElements = \ [ Element('changeOfBitstring', NotificationParametersChangeOfBitstring, 0) @@ -2280,6 +2299,7 @@ class NotificationParameters(Choice): , Element('unsignedOutOfRange', NotificationParametersUnsignedOutOfRangeType, 16) , Element('changeOfCharacterString', NotificationParametersChangeOfCharacterStringType, 17) , Element('changeOfStatusFlags', NotificationParametersChangeOfStatusFlagsType, 18) + , Element('changeOfReliability', NotificationParametersChangeOfReliabilityType, 19) ] class ObjectPropertyValue(Sequence): diff --git a/py25/bacpypes/bvllservice.py b/py25/bacpypes/bvllservice.py index fe1fe29..af4128c 100755 --- a/py25/bacpypes/bvllservice.py +++ b/py25/bacpypes/bvllservice.py @@ -102,6 +102,7 @@ class UDPMultiplexer: bind(self.direct, self.broadcastPort) else: self.broadcast = None + self.broadcastPort = None # create and bind the Annex H and J servers self.annexH = _MultiplexServer(self) diff --git a/py25/bacpypes/constructeddata.py b/py25/bacpypes/constructeddata.py index 1d1cade..f06d2ee 100755 --- a/py25/bacpypes/constructeddata.py +++ b/py25/bacpypes/constructeddata.py @@ -565,7 +565,7 @@ def ArrayOf(klass): def __setitem__(self, item, value): # no wrapping index - if (item < 1) or (item > self.value[0]): + if (item < 0) or (item > self.value[0]): raise IndexError("index out of range") # special length handling for index 0 @@ -575,7 +575,11 @@ def ArrayOf(klass): self.value = self.value[0:value + 1] elif value > self.value[0]: # extend - self.value.extend( [None] * (value - self.value[0]) ) + if issubclass(self.subtype, Atomic): + self.value.extend( [self.subtype().value] * (value - self.value[0]) ) + else: + for i in range(value - self.value[0]): + self.value.append(self.subtype()) else: return self.value[0] = value diff --git a/py25/bacpypes/iocb.py b/py25/bacpypes/iocb.py index 880e2ce..6906d3c 100644 --- a/py25/bacpypes/iocb.py +++ b/py25/bacpypes/iocb.py @@ -723,6 +723,7 @@ class IOQController(IOController): # if there was an error, abort the request if err: + if _debug: IOQController._debug(" - aborting") self.abort_io(iocb, err) def process_io(self, iocb): @@ -767,7 +768,7 @@ class IOQController(IOController): # schedule a call in the future task = FunctionTask(IOQController._wait_trigger, self) - task.install_task(delay=self.wait_time) + task.install_task(delta=self.wait_time) else: # change our state diff --git a/py25/bacpypes/object.py b/py25/bacpypes/object.py index 7cc7b32..3bbac83 100755 --- a/py25/bacpypes/object.py +++ b/py25/bacpypes/object.py @@ -167,6 +167,7 @@ class Property(Logging): # get the value value = obj._values[self.identifier] + if _debug: Property._debug(" - value: %r", value) # access an array if arrayIndex is not None: @@ -200,14 +201,61 @@ class Property(Logging): if not self.mutable: raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + # if changing the length of the array, the value is unsigned + if arrayIndex == 0: + if not Unsigned.is_valid(value): + raise InvalidParameterDatatype("length of %s must be unsigned" % ( + self.identifier, + )) + # if it's atomic, make sure it's valid - if issubclass(self.datatype, Atomic): + elif issubclass(self.datatype, Atomic): if _debug: Property._debug(" - property is atomic, checking value") if not self.datatype.is_valid(value): raise InvalidParameterDatatype("%s must be of type %s" % ( self.identifier, self.datatype.__name__, )) + # if it's an array, make sure it's valid regarding arrayIndex provided + elif issubclass(self.datatype, Array): + if _debug: Property._debug(" - property is array, checking subtype and index") + + # changing a single element + if arrayIndex is not None: + # if it's atomic, make sure it's valid + if issubclass(self.datatype.subtype, Atomic): + if _debug: Property._debug(" - subtype is atomic, checking value") + if not self.datatype.subtype.is_valid(value): + raise InvalidParameterDatatype("%s must be of type %s" % ( + self.identifier, self.datatype.__name__, + )) + # constructed type + elif not isinstance(value, self.datatype.subtype): + raise InvalidParameterDatatype("%s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # replacing the array + elif isinstance(value, list): + # check validity regarding subtype + for item in value: + # if it's atomic, make sure it's valid + if issubclass(self.datatype.subtype, Atomic): + if _debug: Property._debug(" - subtype is atomic, checking value") + if not self.datatype.subtype.is_valid(item): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__, + )) + # constructed type + elif not isinstance(item, self.datatype.subtype): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # value is mutated into a new array + value = self.datatype(value) + + # some kind of constructed data elif not isinstance(value, self.datatype): if _debug: Property._debug(" - property is not atomic and wrong type") raise InvalidParameterDatatype("%s must be of type %s" % ( @@ -389,13 +437,6 @@ class Object(Logging): # empty list of property monitors self._property_monitors = defaultdict(list) - # start with a clean array of property identifiers - if 'propertyList' in initargs: - propertyList = None - else: - propertyList = ArrayOf(PropertyIdentifier)() - initargs['propertyList'] = propertyList - # initialize the object for propid, prop in self._properties.items(): if propid in initargs: @@ -404,20 +445,12 @@ class Object(Logging): # defer to the property object for error checking prop.WriteProperty(self, initargs[propid], direct=True) - # add it to the property list if we are building one - if propertyList is not None: - propertyList.append(propid) - elif prop.default is not None: if _debug: Object._debug(" - setting %s from default", propid) # default values bypass property interface self._values[propid] = prop.default - # add it to the property list if we are building one - if propertyList is not None: - propertyList.append(propid) - else: if not prop.optional: if _debug: Object._debug(" - %s value required", propid) @@ -478,19 +511,12 @@ class Object(Logging): self._properties[prop.identifier] = prop self._values[prop.identifier] = prop.default - # tell the object it has a new property - if 'propertyList' in self._values: - property_list = self.propertyList - if prop.identifier not in property_list: - if _debug: Object._debug(" - adding to property list") - property_list.append(prop.identifier) - def delete_property(self, prop): """Delete a property from an object. The property is an instance of a Property or one of its derived classes, but only the property is relavent. Deleting a property disconnects it from the collection of properties common to all of the objects of its class.""" - if _debug: Object._debug("delete_property %r", value) + if _debug: Object._debug("delete_property %r", prop) # make a copy of the properties dictionary self._properties = _copy(self._properties) @@ -500,13 +526,6 @@ class Object(Logging): if prop.identifier in self._values: del self._values[prop.identifier] - # remove the property identifier from its list of know properties - if 'propertyList' in self._values: - property_list = self.propertyList - if prop.identifier in property_list: - if _debug: Object._debug(" - removing from property list") - property_list.remove(prop.identifier) - def ReadProperty(self, propid, arrayIndex=None): if _debug: Object._debug("ReadProperty %r arrayIndex=%r", propid, arrayIndex) diff --git a/py25/bacpypes/service/device.py b/py25/bacpypes/service/device.py index ac90405..87e618f 100644 --- a/py25/bacpypes/service/device.py +++ b/py25/bacpypes/service/device.py @@ -14,6 +14,8 @@ from ..object import register_object_type, registered_object_types, \ Property, DeviceObject from ..task import FunctionTask +from .object import CurrentPropertyListMixIn + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -25,7 +27,7 @@ _log = ModuleLogger(globals()) class CurrentDateProperty(Property): def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=None, optional=True, mutable=False) + Property.__init__(self, identifier, Date, default=(), optional=True, mutable=False) def ReadProperty(self, obj, arrayIndex=None): # access an array @@ -47,7 +49,7 @@ class CurrentDateProperty(Property): class CurrentTimeProperty(Property): def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=None, optional=True, mutable=False) + Property.__init__(self, identifier, Time, default=(), optional=True, mutable=False) def ReadProperty(self, obj, arrayIndex=None): # access an array @@ -66,7 +68,7 @@ class CurrentTimeProperty(Property): # LocalDeviceObject # -class LocalDeviceObject(DeviceObject): +class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): properties = \ [ CurrentTimeProperty('localTime') @@ -107,6 +109,18 @@ class LocalDeviceObject(DeviceObject): if 'localTime' in kwargs: raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") + # the object identifier is required for the object list + if 'objectIdentifier' not in kwargs: + raise RuntimeError("objectIdentifier is required") + + # the object list is provided + if 'objectList' in kwargs: + raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") + else: + kwargs['objectList'] = ArrayOf(ObjectIdentifier)([ + kwargs['objectIdentifier'], + ]) + # check for a minimum value if kwargs['maxApduLengthAccepted'] < 50: raise ValueError("invalid max APDU length accepted") @@ -115,20 +129,7 @@ class LocalDeviceObject(DeviceObject): if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) # proceed as usual - DeviceObject.__init__(self, **kwargs) - - # create a default implementation of an object list for local devices. - # If it is specified in the kwargs, that overrides this default. - if ('objectList' not in kwargs): - self.objectList = ArrayOf(ObjectIdentifier)([self.objectIdentifier]) - - # if the object has a property list and one wasn't provided - # in the kwargs, then it was created by default and the objectList - # property should be included - if ('propertyList' not in kwargs) and self.propertyList: - # make sure it's not already there - if 'objectList' not in self.propertyList: - self.propertyList.append('objectList') + super(LocalDeviceObject, self).__init__(**kwargs) bacpypes_debugging(LocalDeviceObject) diff --git a/py25/bacpypes/service/object.py b/py25/bacpypes/service/object.py index 60f87e8..ae8e7da 100755 --- a/py25/bacpypes/service/object.py +++ b/py25/bacpypes/service/object.py @@ -3,20 +3,74 @@ from ..debugging import bacpypes_debugging, ModuleLogger from ..capability import Capability -from ..basetypes import ErrorType +from ..basetypes import ErrorType, PropertyIdentifier from ..primitivedata import Atomic, Null, Unsigned -from ..constructeddata import Any, Array +from ..constructeddata import Any, Array, ArrayOf from ..apdu import Error, \ SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ ReadAccessResult, ReadAccessResultElement, ReadAccessResultElementChoice from ..errors import ExecutionError -from ..object import PropertyError +from ..object import Property, Object, PropertyError # some debugging _debug = 0 _log = ModuleLogger(globals()) +# handy reference +ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) + +# +# CurrentPropertyList +# + +@bacpypes_debugging +class CurrentPropertyList(Property): + + def __init__(self): + if _debug: CurrentPropertyList._debug("__init__") + Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) + + # make a list of the properties that have values + property_list = [k for k, v in obj._values.items() + if v is not None + and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') + ] + if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) + + # sort the list so it's stable + property_list.sort() + + # asking for the whole thing + if arrayIndex is None: + return ArrayOfPropertyIdentifier(property_list) + + # asking for the length + if arrayIndex == 0: + return len(property_list) + + # asking for an index + if arrayIndex > len(property_list): + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + return property_list[arrayIndex - 1] + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentPropertyListMixIn +# + +@bacpypes_debugging +class CurrentPropertyListMixIn(Object): + + properties = [ + CurrentPropertyList(), + ] + # # ReadProperty and WriteProperty Services # diff --git a/py25/bacpypes/udp.py b/py25/bacpypes/udp.py index b917ad8..2ea59dd 100755 --- a/py25/bacpypes/udp.py +++ b/py25/bacpypes/udp.py @@ -253,6 +253,8 @@ class UDPDirector(asyncore.dispatcher, Server, ServiceAccessPoint): if _debug: UDPDirector._debug("close_socket") self.socket.close() + self.close() + self.socket = None def handle_close(self): """Remove this from the monitor when it's closed.""" diff --git a/py27/bacpypes/__init__.py b/py27/bacpypes/__init__.py index c391a4b..f72f8b7 100755 --- a/py27/bacpypes/__init__.py +++ b/py27/bacpypes/__init__.py @@ -18,7 +18,7 @@ if _sys.platform not in _supported_platforms: # Project Metadata # -__version__ = '0.16.4' +__version__ = '0.16.5' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py27/bacpypes/basetypes.py b/py27/bacpypes/basetypes.py index 18b1371..b5485f9 100755 --- a/py27/bacpypes/basetypes.py +++ b/py27/bacpypes/basetypes.py @@ -513,6 +513,7 @@ class EngineeringUnits(Enumerated): , 'megavoltAmpereHoursReactive':244 #Mvarh , 'voltsPerDegreeKelvin':176 , 'voltsPerMeter':177 + , 'voltsSquareHours':245 , 'degreesPhase':14 , 'powerFactor':15 , 'webers':178 @@ -897,6 +898,7 @@ class EventType(Enumerated): , 'unsignedOutOfRange':16 , 'changeOfCharacterstring':17 , 'changeOfStatusFlags':18 + , 'changeOfReliability':19 } class FaultType(Enumerated): @@ -1470,7 +1472,18 @@ class Reliability(Enumerated): , 'multiStateFault':9 , 'configurationError':10 , 'communicationFailure':12 - , 'numberFault':13 + , 'memberFault': 13 + , 'monitoredObjectFault': 14 + , 'tripped': 15 + , 'lampFailure': 16 + , 'activationFailure': 17 + , 'renewDHCPFailure': 18 + , 'renewFDRegistration-failure': 19 + , 'restartAutoNegotiationFailure': 20 + , 'restartFailure': 21 + , 'proprietaryCommandFailure': 22 + , 'faultsListed': 23 + , 'referencedObjectFault': 24 } class RestartReason(Enumerated): @@ -2260,6 +2273,13 @@ class NotificationParametersChangeOfStatusFlagsType(Sequence): , Element('referencedFlags', StatusFlags, 1) ] +class NotificationParametersChangeOfReliabilityType(Sequence): + sequenceElements = \ + [ Element('reliability', Reliability, 0) + , Element('statusFlags', StatusFlags, 1) + , Element('propertyValues', SequenceOf(PropertyValue), 2) + ] + class NotificationParameters(Choice): choiceElements = \ [ Element('changeOfBitstring', NotificationParametersChangeOfBitstring, 0) @@ -2279,6 +2299,7 @@ class NotificationParameters(Choice): , Element('unsignedOutOfRange', NotificationParametersUnsignedOutOfRangeType, 16) , Element('changeOfCharacterString', NotificationParametersChangeOfCharacterStringType, 17) , Element('changeOfStatusFlags', NotificationParametersChangeOfStatusFlagsType, 18) + , Element('changeOfReliability', NotificationParametersChangeOfReliabilityType, 19) ] class ObjectPropertyValue(Sequence): diff --git a/py27/bacpypes/bvllservice.py b/py27/bacpypes/bvllservice.py index 56f41d5..5abb24e 100755 --- a/py27/bacpypes/bvllservice.py +++ b/py27/bacpypes/bvllservice.py @@ -103,6 +103,7 @@ class UDPMultiplexer: bind(self.direct, self.broadcastPort) else: self.broadcast = None + self.broadcastPort = None # create and bind the Annex H and J servers self.annexH = _MultiplexServer(self) diff --git a/py27/bacpypes/constructeddata.py b/py27/bacpypes/constructeddata.py index 38e491c..88fe28d 100755 --- a/py27/bacpypes/constructeddata.py +++ b/py27/bacpypes/constructeddata.py @@ -563,7 +563,7 @@ def ArrayOf(klass): def __setitem__(self, item, value): # no wrapping index - if (item < 1) or (item > self.value[0]): + if (item < 0) or (item > self.value[0]): raise IndexError("index out of range") # special length handling for index 0 @@ -573,7 +573,11 @@ def ArrayOf(klass): self.value = self.value[0:value + 1] elif value > self.value[0]: # extend - self.value.extend( [None] * (value - self.value[0]) ) + if issubclass(self.subtype, Atomic): + self.value.extend( [self.subtype().value] * (value - self.value[0]) ) + else: + for i in range(value - self.value[0]): + self.value.append(self.subtype()) else: return self.value[0] = value diff --git a/py27/bacpypes/iocb.py b/py27/bacpypes/iocb.py index 02d0880..db96cba 100644 --- a/py27/bacpypes/iocb.py +++ b/py27/bacpypes/iocb.py @@ -209,7 +209,7 @@ class IOCB(DebugContents): self.ioTimeout = FunctionTask(self.abort, err) # (re)schedule it - self.ioTimeout.install_task(delay=delay) + self.ioTimeout.install_task(delta=delay) def __repr__(self): xid = id(self) @@ -718,6 +718,7 @@ class IOQController(IOController): # if there was an error, abort the request if err: + if _debug: IOQController._debug(" - aborting") self.abort_io(iocb, err) def process_io(self, iocb): @@ -762,7 +763,7 @@ class IOQController(IOController): # schedule a call in the future task = FunctionTask(IOQController._wait_trigger, self) - task.install_task(delay=self.wait_time) + task.install_task(delta=self.wait_time) else: # change our state diff --git a/py27/bacpypes/object.py b/py27/bacpypes/object.py index 9de474f..2cb5648 100755 --- a/py27/bacpypes/object.py +++ b/py27/bacpypes/object.py @@ -81,6 +81,7 @@ def register_object_type(cls=None, vendor_id=0): # build a property dictionary by going through the class and all its parents _properties = {} for c in cls.__mro__: + if _debug: register_object_type._debug(" - c: %r", c) for prop in getattr(c, 'properties', []): if prop.identifier not in _properties: _properties[prop.identifier] = prop @@ -168,6 +169,7 @@ class Property: # get the value value = obj._values[self.identifier] + if _debug: Property._debug(" - value: %r", value) # access an array if arrayIndex is not None: @@ -201,14 +203,61 @@ class Property: if not self.mutable: raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + # if changing the length of the array, the value is unsigned + if arrayIndex == 0: + if not Unsigned.is_valid(value): + raise InvalidParameterDatatype("length of %s must be unsigned" % ( + self.identifier, + )) + # if it's atomic, make sure it's valid - if issubclass(self.datatype, Atomic): + elif issubclass(self.datatype, Atomic): if _debug: Property._debug(" - property is atomic, checking value") if not self.datatype.is_valid(value): raise InvalidParameterDatatype("%s must be of type %s" % ( self.identifier, self.datatype.__name__, )) + # if it's an array, make sure it's valid regarding arrayIndex provided + elif issubclass(self.datatype, Array): + if _debug: Property._debug(" - property is array, checking subtype and index") + + # changing a single element + if arrayIndex is not None: + # if it's atomic, make sure it's valid + if issubclass(self.datatype.subtype, Atomic): + if _debug: Property._debug(" - subtype is atomic, checking value") + if not self.datatype.subtype.is_valid(value): + raise InvalidParameterDatatype("%s must be of type %s" % ( + self.identifier, self.datatype.__name__, + )) + # constructed type + elif not isinstance(value, self.datatype.subtype): + raise InvalidParameterDatatype("%s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # replacing the array + elif isinstance(value, list): + # check validity regarding subtype + for item in value: + # if it's atomic, make sure it's valid + if issubclass(self.datatype.subtype, Atomic): + if _debug: Property._debug(" - subtype is atomic, checking value") + if not self.datatype.subtype.is_valid(item): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__, + )) + # constructed type + elif not isinstance(item, self.datatype.subtype): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # value is mutated into a new array + value = self.datatype(value) + + # some kind of constructed data elif not isinstance(value, self.datatype): if _debug: Property._debug(" - property is not atomic and wrong type") raise InvalidParameterDatatype("%s must be of type %s" % ( @@ -396,13 +445,6 @@ class Object(object): # empty list of property monitors self._property_monitors = defaultdict(list) - # start with a clean array of property identifiers - if 'propertyList' in initargs: - propertyList = None - else: - propertyList = ArrayOf(PropertyIdentifier)() - initargs['propertyList'] = propertyList - # initialize the object for propid, prop in self._properties.items(): if propid in initargs: @@ -411,20 +453,12 @@ class Object(object): # defer to the property object for error checking prop.WriteProperty(self, initargs[propid], direct=True) - # add it to the property list if we are building one - if propertyList is not None: - propertyList.append(propid) - elif prop.default is not None: if _debug: Object._debug(" - setting %s from default", propid) # default values bypass property interface self._values[propid] = prop.default - # add it to the property list if we are building one - if propertyList is not None: - propertyList.append(propid) - else: if not prop.optional: if _debug: Object._debug(" - %s value required", propid) @@ -485,19 +519,12 @@ class Object(object): self._properties[prop.identifier] = prop self._values[prop.identifier] = prop.default - # tell the object it has a new property - if 'propertyList' in self._values: - property_list = self.propertyList - if prop.identifier not in property_list: - if _debug: Object._debug(" - adding to property list") - property_list.append(prop.identifier) - def delete_property(self, prop): """Delete a property from an object. The property is an instance of a Property or one of its derived classes, but only the property is relavent. Deleting a property disconnects it from the collection of properties common to all of the objects of its class.""" - if _debug: Object._debug("delete_property %r", value) + if _debug: Object._debug("delete_property %r", prop) # make a copy of the properties dictionary self._properties = _copy(self._properties) @@ -507,13 +534,6 @@ class Object(object): if prop.identifier in self._values: del self._values[prop.identifier] - # remove the property identifier from its list of know properties - if 'propertyList' in self._values: - property_list = self.propertyList - if prop.identifier in property_list: - if _debug: Object._debug(" - removing from property list") - property_list.remove(prop.identifier) - def ReadProperty(self, propid, arrayIndex=None): if _debug: Object._debug("ReadProperty %r arrayIndex=%r", propid, arrayIndex) diff --git a/py27/bacpypes/service/device.py b/py27/bacpypes/service/device.py index 0382f19..647714b 100644 --- a/py27/bacpypes/service/device.py +++ b/py27/bacpypes/service/device.py @@ -14,6 +14,8 @@ from ..object import register_object_type, registered_object_types, \ Property, DeviceObject from ..task import FunctionTask +from .object import CurrentPropertyListMixIn + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -25,7 +27,7 @@ _log = ModuleLogger(globals()) class CurrentDateProperty(Property): def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=None, optional=True, mutable=False) + Property.__init__(self, identifier, Date, default=(), optional=True, mutable=False) def ReadProperty(self, obj, arrayIndex=None): # access an array @@ -47,7 +49,7 @@ class CurrentDateProperty(Property): class CurrentTimeProperty(Property): def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=None, optional=True, mutable=False) + Property.__init__(self, identifier, Time, default=(), optional=True, mutable=False) def ReadProperty(self, obj, arrayIndex=None): # access an array @@ -67,7 +69,7 @@ class CurrentTimeProperty(Property): # @bacpypes_debugging -class LocalDeviceObject(DeviceObject): +class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): properties = \ [ CurrentTimeProperty('localTime') @@ -102,12 +104,24 @@ class LocalDeviceObject(DeviceObject): raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) - # check for local time + # check for properties this class implements if 'localDate' in kwargs: raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") if 'localTime' in kwargs: raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") + # the object identifier is required for the object list + if 'objectIdentifier' not in kwargs: + raise RuntimeError("objectIdentifier is required") + + # the object list is provided + if 'objectList' in kwargs: + raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") + else: + kwargs['objectList'] = ArrayOf(ObjectIdentifier)([ + kwargs['objectIdentifier'], + ]) + # check for a minimum value if kwargs['maxApduLengthAccepted'] < 50: raise ValueError("invalid max APDU length accepted") @@ -116,20 +130,7 @@ class LocalDeviceObject(DeviceObject): if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) # proceed as usual - DeviceObject.__init__(self, **kwargs) - - # create a default implementation of an object list for local devices. - # If it is specified in the kwargs, that overrides this default. - if ('objectList' not in kwargs): - self.objectList = ArrayOf(ObjectIdentifier)([self.objectIdentifier]) - - # if the object has a property list and one wasn't provided - # in the kwargs, then it was created by default and the objectList - # property should be included - if ('propertyList' not in kwargs) and self.propertyList: - # make sure it's not already there - if 'objectList' not in self.propertyList: - self.propertyList.append('objectList') + super(LocalDeviceObject, self).__init__(**kwargs) # # Who-Is I-Am Services diff --git a/py27/bacpypes/service/object.py b/py27/bacpypes/service/object.py index 6cbcf13..ca8d3fe 100644 --- a/py27/bacpypes/service/object.py +++ b/py27/bacpypes/service/object.py @@ -3,20 +3,74 @@ from ..debugging import bacpypes_debugging, ModuleLogger from ..capability import Capability -from ..basetypes import ErrorType +from ..basetypes import ErrorType, PropertyIdentifier from ..primitivedata import Atomic, Null, Unsigned -from ..constructeddata import Any, Array +from ..constructeddata import Any, Array, ArrayOf from ..apdu import Error, \ SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ ReadAccessResult, ReadAccessResultElement, ReadAccessResultElementChoice from ..errors import ExecutionError -from ..object import PropertyError +from ..object import Property, Object, PropertyError # some debugging _debug = 0 _log = ModuleLogger(globals()) +# handy reference +ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) + +# +# CurrentPropertyList +# + +@bacpypes_debugging +class CurrentPropertyList(Property): + + def __init__(self): + if _debug: CurrentPropertyList._debug("__init__") + Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) + + # make a list of the properties that have values + property_list = [k for k, v in obj._values.items() + if v is not None + and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') + ] + if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) + + # sort the list so it's stable + property_list.sort() + + # asking for the whole thing + if arrayIndex is None: + return ArrayOfPropertyIdentifier(property_list) + + # asking for the length + if arrayIndex == 0: + return len(property_list) + + # asking for an index + if arrayIndex > len(property_list): + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + return property_list[arrayIndex - 1] + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentPropertyListMixIn +# + +@bacpypes_debugging +class CurrentPropertyListMixIn(Object): + + properties = [ + CurrentPropertyList(), + ] + # # ReadProperty and WriteProperty Services # diff --git a/py27/bacpypes/udp.py b/py27/bacpypes/udp.py index 643c45b..589c5fd 100755 --- a/py27/bacpypes/udp.py +++ b/py27/bacpypes/udp.py @@ -252,6 +252,8 @@ class UDPDirector(asyncore.dispatcher, Server, ServiceAccessPoint): if _debug: UDPDirector._debug("close_socket") self.socket.close() + self.close() + self.socket = None def handle_close(self): """Remove this from the monitor when it's closed.""" diff --git a/py34/bacpypes/__init__.py b/py34/bacpypes/__init__.py index 6692b8e..6c410ec 100755 --- a/py34/bacpypes/__init__.py +++ b/py34/bacpypes/__init__.py @@ -18,7 +18,7 @@ if _sys.platform not in _supported_platforms: # Project Metadata # -__version__ = '0.16.4' +__version__ = '0.16.5' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py34/bacpypes/basetypes.py b/py34/bacpypes/basetypes.py index 5caa303..b5485f9 100755 --- a/py34/bacpypes/basetypes.py +++ b/py34/bacpypes/basetypes.py @@ -898,6 +898,7 @@ class EventType(Enumerated): , 'unsignedOutOfRange':16 , 'changeOfCharacterstring':17 , 'changeOfStatusFlags':18 + , 'changeOfReliability':19 } class FaultType(Enumerated): @@ -1471,7 +1472,18 @@ class Reliability(Enumerated): , 'multiStateFault':9 , 'configurationError':10 , 'communicationFailure':12 - , 'numberFault':13 + , 'memberFault': 13 + , 'monitoredObjectFault': 14 + , 'tripped': 15 + , 'lampFailure': 16 + , 'activationFailure': 17 + , 'renewDHCPFailure': 18 + , 'renewFDRegistration-failure': 19 + , 'restartAutoNegotiationFailure': 20 + , 'restartFailure': 21 + , 'proprietaryCommandFailure': 22 + , 'faultsListed': 23 + , 'referencedObjectFault': 24 } class RestartReason(Enumerated): @@ -2261,6 +2273,13 @@ class NotificationParametersChangeOfStatusFlagsType(Sequence): , Element('referencedFlags', StatusFlags, 1) ] +class NotificationParametersChangeOfReliabilityType(Sequence): + sequenceElements = \ + [ Element('reliability', Reliability, 0) + , Element('statusFlags', StatusFlags, 1) + , Element('propertyValues', SequenceOf(PropertyValue), 2) + ] + class NotificationParameters(Choice): choiceElements = \ [ Element('changeOfBitstring', NotificationParametersChangeOfBitstring, 0) @@ -2280,6 +2299,7 @@ class NotificationParameters(Choice): , Element('unsignedOutOfRange', NotificationParametersUnsignedOutOfRangeType, 16) , Element('changeOfCharacterString', NotificationParametersChangeOfCharacterStringType, 17) , Element('changeOfStatusFlags', NotificationParametersChangeOfStatusFlagsType, 18) + , Element('changeOfReliability', NotificationParametersChangeOfReliabilityType, 19) ] class ObjectPropertyValue(Sequence): diff --git a/py34/bacpypes/bvllservice.py b/py34/bacpypes/bvllservice.py index a4d99e8..6abb5d7 100755 --- a/py34/bacpypes/bvllservice.py +++ b/py34/bacpypes/bvllservice.py @@ -103,6 +103,7 @@ class UDPMultiplexer: bind(self.direct, self.broadcastPort) else: self.broadcast = None + self.broadcastPort = None # create and bind the Annex H and J servers self.annexH = _MultiplexServer(self) diff --git a/py34/bacpypes/constructeddata.py b/py34/bacpypes/constructeddata.py index 54b9f5a..88fe28d 100755 --- a/py34/bacpypes/constructeddata.py +++ b/py34/bacpypes/constructeddata.py @@ -563,7 +563,7 @@ def ArrayOf(klass): def __setitem__(self, item, value): # no wrapping index - if (item < 1) or (item > self.value[0]): + if (item < 0) or (item > self.value[0]): raise IndexError("index out of range") # special length handling for index 0 @@ -573,7 +573,11 @@ def ArrayOf(klass): self.value = self.value[0:value + 1] elif value > self.value[0]: # extend - self.value.extend( [None] * (value - self.value[0]) ) + if issubclass(self.subtype, Atomic): + self.value.extend( [self.subtype().value] * (value - self.value[0]) ) + else: + for i in range(value - self.value[0]): + self.value.append(self.subtype()) else: return self.value[0] = value @@ -924,7 +928,7 @@ class Choice(object): # check for the correct closing tag tag = taglist.Pop() if tag.tagClass != Tag.closingTagClass or tag.tagNumber != element.context: - raise DecodingError("'%s' expected closing tag %d" % (element.name, element.context)) + raise InvalidTag("%s expected closing tag %d" % (element.name, element.context)) # done if _debug: Choice._debug(" - found choice (structure)") diff --git a/py34/bacpypes/object.py b/py34/bacpypes/object.py index cc28ad7..9f31dd9 100755 --- a/py34/bacpypes/object.py +++ b/py34/bacpypes/object.py @@ -168,6 +168,7 @@ class Property: # get the value value = obj._values[self.identifier] + if _debug: Property._debug(" - value: %r", value) # access an array if arrayIndex is not None: @@ -201,14 +202,61 @@ class Property: if not self.mutable: raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + # if changing the length of the array, the value is unsigned + if arrayIndex == 0: + if not Unsigned.is_valid(value): + raise InvalidParameterDatatype("length of %s must be unsigned" % ( + self.identifier, + )) + # if it's atomic, make sure it's valid - if issubclass(self.datatype, Atomic): + elif issubclass(self.datatype, Atomic): if _debug: Property._debug(" - property is atomic, checking value") if not self.datatype.is_valid(value): raise InvalidParameterDatatype("%s must be of type %s" % ( self.identifier, self.datatype.__name__, )) + # if it's an array, make sure it's valid regarding arrayIndex provided + elif issubclass(self.datatype, Array): + if _debug: Property._debug(" - property is array, checking subtype and index") + + # changing a single element + if arrayIndex is not None: + # if it's atomic, make sure it's valid + if issubclass(self.datatype.subtype, Atomic): + if _debug: Property._debug(" - subtype is atomic, checking value") + if not self.datatype.subtype.is_valid(value): + raise InvalidParameterDatatype("%s must be of type %s" % ( + self.identifier, self.datatype.__name__, + )) + # constructed type + elif not isinstance(value, self.datatype.subtype): + raise InvalidParameterDatatype("%s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # replacing the array + elif isinstance(value, list): + # check validity regarding subtype + for item in value: + # if it's atomic, make sure it's valid + if issubclass(self.datatype.subtype, Atomic): + if _debug: Property._debug(" - subtype is atomic, checking value") + if not self.datatype.subtype.is_valid(item): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__, + )) + # constructed type + elif not isinstance(item, self.datatype.subtype): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # value is mutated into a new array + value = self.datatype(value) + + # some kind of constructed data elif not isinstance(value, self.datatype): if _debug: Property._debug(" - property is not atomic and wrong type") raise InvalidParameterDatatype("%s must be of type %s" % ( @@ -396,13 +444,6 @@ class Object: # empty list of property monitors self._property_monitors = defaultdict(list) - # start with a clean array of property identifiers - if 'propertyList' in initargs: - propertyList = None - else: - propertyList = ArrayOf(PropertyIdentifier)() - initargs['propertyList'] = propertyList - # initialize the object for propid, prop in self._properties.items(): if propid in initargs: @@ -411,20 +452,12 @@ class Object: # defer to the property object for error checking prop.WriteProperty(self, initargs[propid], direct=True) - # add it to the property list if we are building one - if propertyList is not None: - propertyList.append(propid) - elif prop.default is not None: if _debug: Object._debug(" - setting %s from default", propid) # default values bypass property interface self._values[propid] = prop.default - # add it to the property list if we are building one - if propertyList is not None: - propertyList.append(propid) - else: if not prop.optional: if _debug: Object._debug(" - %s value required", propid) @@ -485,19 +518,12 @@ class Object: self._properties[prop.identifier] = prop self._values[prop.identifier] = prop.default - # tell the object it has a new property - if 'propertyList' in self._values: - property_list = self.propertyList - if prop.identifier not in property_list: - if _debug: Object._debug(" - adding to property list") - property_list.append(prop.identifier) - def delete_property(self, prop): """Delete a property from an object. The property is an instance of a Property or one of its derived classes, but only the property is relavent. Deleting a property disconnects it from the collection of properties common to all of the objects of its class.""" - if _debug: Object._debug("delete_property %r", value) + if _debug: Object._debug("delete_property %r", prop) # make a copy of the properties dictionary self._properties = _copy(self._properties) @@ -507,13 +533,6 @@ class Object: if prop.identifier in self._values: del self._values[prop.identifier] - # remove the property identifier from its list of know properties - if 'propertyList' in self._values: - property_list = self.propertyList - if prop.identifier in property_list: - if _debug: Object._debug(" - removing from property list") - property_list.remove(prop.identifier) - def ReadProperty(self, propid, arrayIndex=None): if _debug: Object._debug("ReadProperty %r arrayIndex=%r", propid, arrayIndex) diff --git a/py34/bacpypes/service/device.py b/py34/bacpypes/service/device.py index 0382f19..f8871c7 100644 --- a/py34/bacpypes/service/device.py +++ b/py34/bacpypes/service/device.py @@ -14,6 +14,8 @@ from ..object import register_object_type, registered_object_types, \ Property, DeviceObject from ..task import FunctionTask +from .object import CurrentPropertyListMixIn + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -25,7 +27,7 @@ _log = ModuleLogger(globals()) class CurrentDateProperty(Property): def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=None, optional=True, mutable=False) + Property.__init__(self, identifier, Date, default=(), optional=True, mutable=False) def ReadProperty(self, obj, arrayIndex=None): # access an array @@ -47,7 +49,7 @@ class CurrentDateProperty(Property): class CurrentTimeProperty(Property): def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=None, optional=True, mutable=False) + Property.__init__(self, identifier, Time, default=(), optional=True, mutable=False) def ReadProperty(self, obj, arrayIndex=None): # access an array @@ -67,7 +69,7 @@ class CurrentTimeProperty(Property): # @bacpypes_debugging -class LocalDeviceObject(DeviceObject): +class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): properties = \ [ CurrentTimeProperty('localTime') @@ -108,6 +110,18 @@ class LocalDeviceObject(DeviceObject): if 'localTime' in kwargs: raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") + # the object identifier is required for the object list + if 'objectIdentifier' not in kwargs: + raise RuntimeError("objectIdentifier is required") + + # the object list is provided + if 'objectList' in kwargs: + raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") + else: + kwargs['objectList'] = ArrayOf(ObjectIdentifier)([ + kwargs['objectIdentifier'], + ]) + # check for a minimum value if kwargs['maxApduLengthAccepted'] < 50: raise ValueError("invalid max APDU length accepted") @@ -116,20 +130,7 @@ class LocalDeviceObject(DeviceObject): if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) # proceed as usual - DeviceObject.__init__(self, **kwargs) - - # create a default implementation of an object list for local devices. - # If it is specified in the kwargs, that overrides this default. - if ('objectList' not in kwargs): - self.objectList = ArrayOf(ObjectIdentifier)([self.objectIdentifier]) - - # if the object has a property list and one wasn't provided - # in the kwargs, then it was created by default and the objectList - # property should be included - if ('propertyList' not in kwargs) and self.propertyList: - # make sure it's not already there - if 'objectList' not in self.propertyList: - self.propertyList.append('objectList') + super(LocalDeviceObject, self).__init__(**kwargs) # # Who-Is I-Am Services diff --git a/py34/bacpypes/service/object.py b/py34/bacpypes/service/object.py index 6cbcf13..ca8d3fe 100755 --- a/py34/bacpypes/service/object.py +++ b/py34/bacpypes/service/object.py @@ -3,20 +3,74 @@ from ..debugging import bacpypes_debugging, ModuleLogger from ..capability import Capability -from ..basetypes import ErrorType +from ..basetypes import ErrorType, PropertyIdentifier from ..primitivedata import Atomic, Null, Unsigned -from ..constructeddata import Any, Array +from ..constructeddata import Any, Array, ArrayOf from ..apdu import Error, \ SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ ReadAccessResult, ReadAccessResultElement, ReadAccessResultElementChoice from ..errors import ExecutionError -from ..object import PropertyError +from ..object import Property, Object, PropertyError # some debugging _debug = 0 _log = ModuleLogger(globals()) +# handy reference +ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) + +# +# CurrentPropertyList +# + +@bacpypes_debugging +class CurrentPropertyList(Property): + + def __init__(self): + if _debug: CurrentPropertyList._debug("__init__") + Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) + + # make a list of the properties that have values + property_list = [k for k, v in obj._values.items() + if v is not None + and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') + ] + if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) + + # sort the list so it's stable + property_list.sort() + + # asking for the whole thing + if arrayIndex is None: + return ArrayOfPropertyIdentifier(property_list) + + # asking for the length + if arrayIndex == 0: + return len(property_list) + + # asking for an index + if arrayIndex > len(property_list): + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + return property_list[arrayIndex - 1] + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentPropertyListMixIn +# + +@bacpypes_debugging +class CurrentPropertyListMixIn(Object): + + properties = [ + CurrentPropertyList(), + ] + # # ReadProperty and WriteProperty Services # diff --git a/py34/bacpypes/udp.py b/py34/bacpypes/udp.py index d2c1063..47ede29 100755 --- a/py34/bacpypes/udp.py +++ b/py34/bacpypes/udp.py @@ -252,6 +252,8 @@ class UDPDirector(asyncore.dispatcher, Server, ServiceAccessPoint): if _debug: UDPDirector._debug("close_socket") self.socket.close() + self.close() + self.socket = None def handle_close(self): """Remove this from the monitor when it's closed.""" diff --git a/samples/ReadPropertyMultipleServer.py b/samples/ReadPropertyMultipleServer.py index 8cc0e80..961bd63 100755 --- a/samples/ReadPropertyMultipleServer.py +++ b/samples/ReadPropertyMultipleServer.py @@ -12,7 +12,8 @@ from bacpypes.consolelogging import ConfigArgumentParser from bacpypes.core import run -from bacpypes.primitivedata import Real +from bacpypes.primitivedata import Real, CharacterString +from bacpypes.constructeddata import ArrayOf from bacpypes.object import AnalogValueObject, Property, register_object_type from bacpypes.errors import ExecutionError @@ -73,6 +74,7 @@ class RandomAnalogValueObject(AnalogValueObject): properties = [ RandomValueProperty('presentValue'), + Property('eventMessageTexts', ArrayOf(CharacterString), mutable=True), ] def __init__(self, **kwargs): @@ -107,7 +109,8 @@ def main(): # make a random input object ravo1 = RandomAnalogValueObject( - objectIdentifier=('analogValue', 1), objectName='Random1' + objectIdentifier=('analogValue', 1), objectName='Random1', + eventMessageTexts=ArrayOf(CharacterString)(["hello"]), ) _log.debug(" - ravo1: %r", ravo1) diff --git a/samples/ReadWriteEventMessageTexts.py b/samples/ReadWriteEventMessageTexts.py new file mode 100644 index 0000000..124b0db --- /dev/null +++ b/samples/ReadWriteEventMessageTexts.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python + +""" +This application is similar to ReadWriteProperty but it is just for beating on +the event message texts, an array of character strings. +""" + +import sys + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.core import run, enable_sleeping +from bacpypes.iocb import IOCB + +from bacpypes.pdu import Address +from bacpypes.object import get_datatype + +from bacpypes.apdu import SimpleAckPDU, \ + ReadPropertyRequest, ReadPropertyACK, WritePropertyRequest +from bacpypes.primitivedata import Unsigned, CharacterString +from bacpypes.constructeddata import Array, ArrayOf, Any + +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_application = None +context = None + +# +# ReadWritePropertyConsoleCmd +# + +@bacpypes_debugging +class ReadWritePropertyConsoleCmd(ConsoleCmd): + + def do_read(self, args): + """read [ ]""" + args = args.split() + if _debug: ReadWritePropertyConsoleCmd._debug("do_read %r", args) + global context + + try: + addr, obj_type, obj_inst = context + prop_id = 'eventMessageTexts' + + datatype = get_datatype(obj_type, prop_id) + if not datatype: + raise ValueError("invalid property for object type") + + # build a request + request = ReadPropertyRequest( + objectIdentifier=(obj_type, obj_inst), + propertyIdentifier=prop_id, + ) + request.pduDestination = Address(addr) + + if len(args) == 1: + request.propertyArrayIndex = int(args[0]) + if _debug: ReadWritePropertyConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: ReadWritePropertyConsoleCmd._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, ReadPropertyACK): + if _debug: ReadWritePropertyConsoleCmd._debug(" - not an ack") + return + + # find the datatype + datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) + if _debug: ReadWritePropertyConsoleCmd._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: + value = apdu.propertyValue.cast_out(Unsigned) + else: + value = apdu.propertyValue.cast_out(datatype.subtype) + else: + value = apdu.propertyValue.cast_out(datatype) + if _debug: ReadWritePropertyConsoleCmd._debug(" - value: %r", value) + + sys.stdout.write(str(value) + '\n') + if hasattr(value, 'debug_contents'): + value.debug_contents(file=sys.stdout) + sys.stdout.flush() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') + + except Exception as error: + ReadWritePropertyConsoleCmd._exception("exception: %r", error) + + def do_write(self, args): + """ + write + write 0 + write [ ]... + """ + args = args.split() + ReadWritePropertyConsoleCmd._debug("do_write %r", args) + + try: + addr, obj_type, obj_inst = context + prop_id = 'eventMessageTexts' + + indx = None + if args and args[0].isdigit(): + indx = int(args[0]) + if indx == 0: + value = Unsigned(int(args[1])) + else: + value = CharacterString(args[1]) + else: + value = ArrayOf(CharacterString)(args[0:]) + + # build a request + request = WritePropertyRequest( + objectIdentifier=(obj_type, obj_inst), + propertyIdentifier=prop_id + ) + request.pduDestination = Address(addr) + + # save the value + request.propertyValue = Any() + try: + request.propertyValue.cast_in(value) + except Exception as error: + ReadWritePropertyConsoleCmd._exception("WriteProperty cast error: %r", error) + + # optional array index + if indx is not None: + request.propertyArrayIndex = indx + + if _debug: ReadWritePropertyConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: ReadWritePropertyConsoleCmd._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: + # should be an ack + if not isinstance(iocb.ioResponse, SimpleAckPDU): + if _debug: ReadWritePropertyConsoleCmd._debug(" - not an ack") + return + + sys.stdout.write("ack\n") + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') + + except Exception as error: + ReadWritePropertyConsoleCmd._exception("exception: %r", error) + + def do_rtn(self, args): + """rtn ... """ + args = args.split() + if _debug: ReadWritePropertyConsoleCmd._debug("do_rtn %r", args) + + # safe to assume only one adapter + adapter = this_application.nsap.adapters[0] + if _debug: ReadWritePropertyConsoleCmd._debug(" - adapter: %r", adapter) + + # provide the address and a list of network numbers + router_address = Address(args[0]) + network_list = [int(arg) for arg in args[1:]] + + # pass along to the service access point + this_application.nsap.add_router_references(adapter, router_address, network_list) + + +# +# __main__ +# + +def main(): + global this_application, context + + # parse the command line arguments + parser = ConfigArgumentParser(description=__doc__) + parser.add_argument( + "address", + help="address of server", + ) + parser.add_argument( + "objtype", + help="object type", + ) + parser.add_argument( + "objinst", type=int, + help="object instance", + ) + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # set the context, the collection of the above parameters + context = args.address, args.objtype, args.objinst + if _debug: _log.debug(" - context: %r", context) + + + # make a device object + this_device = LocalDeviceObject( + objectName=args.ini.objectname, + objectIdentifier=int(args.ini.objectidentifier), + maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), + segmentationSupported=args.ini.segmentationsupported, + vendorIdentifier=int(args.ini.vendoridentifier), + ) + + # make a simple application + this_application = BIPSimpleApplication(this_device, args.ini.address) + + # get the services supported + services_supported = this_application.get_services_supported() + if _debug: _log.debug(" - services_supported: %r", services_supported) + + # let the device object know + this_device.protocolServicesSupported = services_supported.value + + # make a console + this_console = ReadWritePropertyConsoleCmd() + if _debug: _log.debug(" - this_console: %r", this_console) + + # enable sleeping will help with threads + enable_sleeping() + + _log.debug("running") + + run() + + _log.debug("fini") + +if __name__ == "__main__": + main() diff --git a/tests/__init__.py b/tests/__init__.py index 3bac612..ac78389 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -17,6 +17,7 @@ from . import trapped_classes from . import test_comm from . import test_pdu from . import test_primitive_data +from . import test_constructed_data from . import test_utilities from . import test_vlan diff --git a/tests/test_constructed_data/__init__.py b/tests/test_constructed_data/__init__.py new file mode 100644 index 0000000..3878647 --- /dev/null +++ b/tests/test_constructed_data/__init__.py @@ -0,0 +1,14 @@ +#!/usr/bin/python + +""" +Test Constructed Data Module +""" + +from . import test_sequence +from . import test_sequence_of +from . import test_array_of +from . import test_choice +from . import test_any +from . import test_any_atomic + + diff --git a/tests/test_constructed_data/helpers.py b/tests/test_constructed_data/helpers.py new file mode 100644 index 0000000..06395b8 --- /dev/null +++ b/tests/test_constructed_data/helpers.py @@ -0,0 +1,88 @@ +#!/usr/bin/python + +""" +Helper classes for constructed data tests. +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob + +from bacpypes.errors import MissingRequiredParameter +from bacpypes.primitivedata import Boolean, Integer, Tag, TagList +from bacpypes.constructeddata import Element, Sequence + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +@bacpypes_debugging +class SequenceEquality: + + """ + This mixin class adds an equality function for matching values for all of + the elements, even if they are optional. It will raise an exception for + missing elements, even if they are missing in both objects. + """ + + def __eq__(self, other): + if _debug: SequenceEquality._debug("__eq__ %r", other) + + # loop through this sequences elements + for element in self.sequenceElements: + self_value = getattr(self, element.name, None) + other_value = getattr(other, element.name, None) + + if (not element.optional) and ((self_value is None) or (other_value is None)): + raise MissingRequiredParameter("%s is a missing required element of %s" % (element.name, self.__class__.__name__)) + if not (self_value == other_value): + return False + + # success + return True + + +@bacpypes_debugging +class EmptySequence(Sequence, SequenceEquality): + + def __init__(self, *args, **kwargs): + if _debug: EmptySequence._debug("__init__ %r %r", args, kwargs) + Sequence.__init__(self, *args, **kwargs) + + +@bacpypes_debugging +class SimpleSequence(Sequence, SequenceEquality): + + sequenceElements = [ + Element('hydrogen', Boolean), + ] + + def __init__(self, *args, **kwargs): + if _debug: SimpleSequence._debug("__init__ %r %r", args, kwargs) + Sequence.__init__(self, *args, **kwargs) + + +@bacpypes_debugging +class CompoundSequence1(Sequence, SequenceEquality): + + sequenceElements = [ + Element('hydrogen', Boolean), + Element('helium', Integer), + ] + + def __init__(self, *args, **kwargs): + if _debug: CompoundSequence1._debug("__init__ %r %r", args, kwargs) + Sequence.__init__(self, *args, **kwargs) + + +@bacpypes_debugging +class CompoundSequence2(Sequence, SequenceEquality): + + sequenceElements = [ + Element('lithium', Boolean, optional=True), + Element('beryllium', Integer), + ] + + def __init__(self, *args, **kwargs): + if _debug: CompoundSequence2._debug("__init__ %r %r", args, kwargs) + Sequence.__init__(self, *args, **kwargs) + diff --git a/tests/test_constructed_data/test_any.py b/tests/test_constructed_data/test_any.py new file mode 100644 index 0000000..f323afe --- /dev/null +++ b/tests/test_constructed_data/test_any.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# placeholder diff --git a/tests/test_constructed_data/test_any_atomic.py b/tests/test_constructed_data/test_any_atomic.py new file mode 100644 index 0000000..f323afe --- /dev/null +++ b/tests/test_constructed_data/test_any_atomic.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# placeholder diff --git a/tests/test_constructed_data/test_array_of.py b/tests/test_constructed_data/test_array_of.py new file mode 100644 index 0000000..8e84833 --- /dev/null +++ b/tests/test_constructed_data/test_array_of.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test Array +---------- +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob + +from bacpypes.errors import MissingRequiredParameter +from bacpypes.primitivedata import Integer, Tag, TagList +from bacpypes.constructeddata import Element, Sequence, ArrayOf + +from .helpers import SimpleSequence + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +# array of integers +IntegerArray = ArrayOf(Integer) + +@bacpypes_debugging +class TestIntegerArray(unittest.TestCase): + + def test_empty_array(self): + if _debug: TestIntegerArray._debug("test_empty_array") + + # create an empty array + ary = IntegerArray() + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # array sematics + assert len(ary) == 0 + assert ary[0] == 0 + + # encode it in a tag list + tag_list = TagList() + ary.encode(tag_list) + if _debug: TestIntegerArray._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + ary = IntegerArray() + ary.decode(tag_list) + if _debug: TestIntegerArray._debug(" - seq: %r", seq) + + def test_append(self): + if _debug: TestIntegerArray._debug("test_append") + + # create an empty array + ary = IntegerArray() + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # append an integer + ary.append(2) + assert len(ary) == 1 + assert ary[0] == 1 + assert ary[1] == 2 + + def test_delete_item(self): + if _debug: TestIntegerArray._debug("test_delete_item") + + # create an array + ary = IntegerArray([1, 2, 3]) + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # delete something + del ary[2] + assert len(ary) == 2 + assert ary[0] == 2 + assert ary.value[1:] == [1, 3] + + def test_index_item(self): + if _debug: TestIntegerArray._debug("test_index_item") + + # create an array + ary = IntegerArray([1, 2, 3]) + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # find something + assert ary.index(3) == 3 + + # not find something + with self.assertRaises(ValueError): + indx = ary.index(4) + + def test_remove_item(self): + if _debug: TestIntegerArray._debug("test_remove_item") + + # create an array + ary = IntegerArray([1, 2, 3]) + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # remove something + ary.remove(2) + assert ary.value[1:] == [1, 3] + + # not remove something + with self.assertRaises(ValueError): + ary.remove(4) + + def test_resize(self): + if _debug: TestIntegerArray._debug("test_resize") + + # create an array + ary = IntegerArray([1, 2, 3]) + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # make it shorter + ary[0] = 2 + assert ary.value[1:] == [1, 2] + + # make it longer + ary[0] = 4 + assert ary.value[1:] == [1, 2, 0, 0] + + def test_get_item(self): + if _debug: TestIntegerArray._debug("test_get_item") + + # create an array + ary = IntegerArray([1, 2, 3]) + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # BACnet semantics + assert ary[1] == 1 + + def test_set_item(self): + if _debug: TestIntegerArray._debug("test_set_item") + + # create an array + ary = IntegerArray([1, 2, 3]) + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # BACnet semantics, no type checking + ary[1] = 10 + assert ary[1] == 10 + + def test_codec(self): + if _debug: TestIntegerArray._debug("test_codec") + + # test array contents + ary_value = [1, 2, 3] + + # create an array + ary = IntegerArray(ary_value) + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # encode it in a tag list + tag_list = TagList() + ary.encode(tag_list) + if _debug: TestIntegerArray._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + ary = IntegerArray() + ary.decode(tag_list) + if _debug: TestIntegerArray._debug(" - ary %r", ary) + + # value matches + assert ary.value[1:] == ary_value + + +# array of a sequence +SimpleSequenceArray = ArrayOf(SimpleSequence) + +@bacpypes_debugging +class TestSimpleSequenceArray(unittest.TestCase): + + def test_codec(self): + if _debug: TestSimpleSequenceArray._debug("test_codec") + + # test array contents + ary_value = [ + SimpleSequence(hydrogen=True), + SimpleSequence(hydrogen=False), + SimpleSequence(hydrogen=True), + ] + + # create an array + ary = SimpleSequenceArray(ary_value) + if _debug: TestSimpleSequenceArray._debug(" - ary: %r", ary) + + # encode it in a tag list + tag_list = TagList() + ary.encode(tag_list) + if _debug: TestSimpleSequenceArray._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + ary = SimpleSequenceArray() + ary.decode(tag_list) + if _debug: TestSimpleSequenceArray._debug(" - ary %r", ary) + + # value matches + assert ary.value[1:] == ary_value + diff --git a/tests/test_constructed_data/test_choice.py b/tests/test_constructed_data/test_choice.py new file mode 100644 index 0000000..f323afe --- /dev/null +++ b/tests/test_constructed_data/test_choice.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# placeholder diff --git a/tests/test_constructed_data/test_sequence.py b/tests/test_constructed_data/test_sequence.py new file mode 100644 index 0000000..512ed3e --- /dev/null +++ b/tests/test_constructed_data/test_sequence.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test Constructed Data Sequence +------------------------------ +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob + +from bacpypes.errors import MissingRequiredParameter +from bacpypes.primitivedata import Boolean, Integer, Tag, TagList +from bacpypes.constructeddata import Element, Sequence + +from .helpers import EmptySequence, SimpleSequence, CompoundSequence1, \ + CompoundSequence2 + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +@bacpypes_debugging +class TestEmptySequence(unittest.TestCase): + + def test_empty_sequence(self): + if _debug: TestEmptySequence._debug("test_empty_sequence") + + # create a sequence + seq = EmptySequence() + if _debug: TestEmptySequence._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + seq.encode(tag_list) + if _debug: TestEmptySequence._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + seq = EmptySequence() + seq.decode(tag_list) + if _debug: TestEmptySequence._debug(" - seq: %r", seq) + + def test_no_elements(self): + if _debug: TestEmptySequence._debug("test_no_elements") + + # create a sequence with an undefined element + with self.assertRaises(TypeError): + seq = EmptySequence(some_element=None) + + +@bacpypes_debugging +class TestSimpleSequence(unittest.TestCase): + + def test_missing_element(self): + if _debug: TestSimpleSequence._debug("test_missing_element") + + # create a sequence with a missing required element + seq = SimpleSequence() + if _debug: TestSimpleSequence._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + with self.assertRaises(MissingRequiredParameter): + seq.encode(tag_list) + + def test_wrong_type(self): + if _debug: TestSimpleSequence._debug("test_wrong_type") + + # create a sequence with wrong element value type + seq = SimpleSequence(hydrogen=12) + with self.assertRaises(TypeError): + tag_list = TagList() + seq.encode(tag_list) + + def test_codec(self): + if _debug: TestSimpleSequence._debug("test_codec") + + # create a sequence + seq = SimpleSequence(hydrogen=False) + if _debug: TestSimpleSequence._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + seq.encode(tag_list) + if _debug: TestSimpleSequence._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + seq = SimpleSequence() + seq.decode(tag_list) + if _debug: TestSimpleSequence._debug(" - seq: %r", seq) + + +@bacpypes_debugging +class TestCompoundSequence1(unittest.TestCase): + + def test_missing_element(self): + if _debug: TestCompoundSequence1._debug("test_missing_element") + + # create a sequence with a missing required element + seq = CompoundSequence1() + if _debug: TestSimpleSequence._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + with self.assertRaises(MissingRequiredParameter): + seq.encode(tag_list) + + # create a sequence with a missing required element + seq = CompoundSequence1(hydrogen=True) + if _debug: TestSimpleSequence._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + with self.assertRaises(MissingRequiredParameter): + seq.encode(tag_list) + + # create a sequence with a missing required element + seq = CompoundSequence1(helium=2) + if _debug: TestSimpleSequence._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + with self.assertRaises(MissingRequiredParameter): + seq.encode(tag_list) + + def test_codec(self): + if _debug: TestCompoundSequence1._debug("test_codec") + + # create a sequence + seq = CompoundSequence1(hydrogen=True, helium=2) + if _debug: TestCompoundSequence1._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + seq.encode(tag_list) + if _debug: TestCompoundSequence1._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + seq = CompoundSequence1() + seq.decode(tag_list) + if _debug: TestCompoundSequence1._debug(" - seq: %r", seq) + + +@bacpypes_debugging +class TestCompoundSequence2(unittest.TestCase): + + def test_missing_element(self): + if _debug: TestCompoundSequence2._debug("test_missing_element") + + # create a sequence with a missing required element + seq = CompoundSequence2() + if _debug: TestCompoundSequence2._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + with self.assertRaises(MissingRequiredParameter): + seq.encode(tag_list) + + # create a sequence with a missing required element + seq = CompoundSequence2(lithium=True) + if _debug: TestCompoundSequence2._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + with self.assertRaises(MissingRequiredParameter): + seq.encode(tag_list) + + def test_codec_1(self): + if _debug: TestCompoundSequence2._debug("test_codec_1") + + # create a sequence + seq = CompoundSequence2(beryllium=2) + if _debug: TestCompoundSequence2._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + seq.encode(tag_list) + if _debug: TestCompoundSequence2._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + seq = CompoundSequence2() + seq.decode(tag_list) + if _debug: TestCompoundSequence2._debug(" - seq: %r", seq) + + def test_codec_2(self): + if _debug: TestCompoundSequence2._debug("test_codec_2") + + # create a sequence + seq = CompoundSequence2(lithium=True, beryllium=3) + if _debug: TestCompoundSequence2._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + seq.encode(tag_list) + if _debug: TestCompoundSequence2._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + seq = CompoundSequence2() + seq.decode(tag_list) + if _debug: TestCompoundSequence2._debug(" - seq: %r", seq) + + diff --git a/tests/test_constructed_data/test_sequence_of.py b/tests/test_constructed_data/test_sequence_of.py new file mode 100644 index 0000000..f323afe --- /dev/null +++ b/tests/test_constructed_data/test_sequence_of.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# placeholder diff --git a/tests/test_service/test_object.py b/tests/test_service/test_object.py index fdffa2a..5204042 100644 --- a/tests/test_service/test_object.py +++ b/tests/test_service/test_object.py @@ -1 +1,256 @@ -# placeholder +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test Object Services +-------------------- +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob + +from bacpypes.errors import ExecutionError, InvalidParameterDatatype +from bacpypes.primitivedata import CharacterString +from bacpypes.constructeddata import ArrayOf +from bacpypes.object import register_object_type, ReadableProperty, \ + WritableProperty, Object + +from bacpypes.service.object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +@bacpypes_debugging +class TestBasic(unittest.TestCase): + + def test_basic(self): + """Test basic configuration of a network.""" + if _debug: TestBasic._debug("test_basic") + + # create an object, no properties + obj = Object() + + +@bacpypes_debugging +@register_object_type(vendor_id=999) +class SampleReadableLocation(Object): + + objectType = 'sampleReadableLocation' + properties = [ + ReadableProperty('location', CharacterString), + ] + + def __init__(self, **kwargs): + if _debug: SampleReadableLocation._debug("__init__ %r", kwargs) + Object.__init__(self, **kwargs) + + +@bacpypes_debugging +class TestReadableLocation(unittest.TestCase): + + def test_sample(self): + """Test basic configuration of a network.""" + if _debug: TestReadableLocation._debug("test_sample") + + # create an object, default property value is None + obj = SampleReadableLocation() + assert obj.location == None + + # create an object with a location + obj = SampleReadableLocation(location="home") + assert obj.ReadProperty('location') == "home" + + # not an array, write access denied + with self.assertRaises(ExecutionError): + obj.ReadProperty('location', 0) + with self.assertRaises(ExecutionError): + obj.WriteProperty('location', "work") + + +@bacpypes_debugging +@register_object_type(vendor_id=999) +class SampleWritableLocation(Object): + + objectType = 'sampleWritableLocation' + properties = [ + WritableProperty('location', CharacterString), + ] + + def __init__(self, **kwargs): + if _debug: SampleWritableLocation._debug("__init__ %r", kwargs) + Object.__init__(self, **kwargs) + + +@bacpypes_debugging +class TestWritableLocation(unittest.TestCase): + + def test_sample(self): + """Test basic configuration of a network.""" + if _debug: TestWritableLocation._debug("test_sample") + + # create an object with a location + obj = SampleWritableLocation(location="home") + assert obj.ReadProperty('location') == "home" + + # not an array, write access denied + with self.assertRaises(ExecutionError): + obj.ReadProperty('location', 0) + + # write access successful + obj.WriteProperty('location', "work") + assert obj.location == "work" + + # wrong data type + with self.assertRaises(InvalidParameterDatatype): + obj.WriteProperty('location', 12) + + +# array of character strings +ArrayOfCharacterString = ArrayOf(CharacterString) + +@bacpypes_debugging +@register_object_type(vendor_id=999) +class SampleWritableArray(Object): + + objectType = 'sampleWritableLocation' + properties = [ + WritableProperty('location', ArrayOfCharacterString), + ] + + def __init__(self, **kwargs): + if _debug: SampleWritableArray._debug("__init__ %r", kwargs) + Object.__init__(self, **kwargs) + + +@bacpypes_debugging +class TestWritableArray(unittest.TestCase): + + def test_empty_array(self): + """Test basic configuration of a network.""" + if _debug: TestWritableArray._debug("test_empty_array") + + # create an object with a location + obj = SampleWritableArray(location=ArrayOfCharacterString()) + if _debug: TestWritableArray._debug(" - obj.location: %r", obj.location) + + assert len(obj.location) == 0 + assert obj.location[0] == 0 + + def test_short_array(self): + if _debug: TestWritableArray._debug("test_short_array") + + # create an object with a location + obj = SampleWritableArray(location=ArrayOfCharacterString(["home"])) + if _debug: TestWritableArray._debug(" - obj.location: %r", obj.location) + + assert obj.ReadProperty('location', 0) == 1 + assert obj.ReadProperty('location', 1) == "home" + + def test_changing_length(self): + if _debug: TestWritableArray._debug("test_changing_length") + + # create an object with a location + obj = SampleWritableArray(location=ArrayOfCharacterString(["home"])) + if _debug: TestWritableArray._debug(" - obj.location: %r", obj.location) + + # change the length of the array + obj.WriteProperty('location', 2, arrayIndex=0) + assert obj.ReadProperty('location', 0) == 2 + + # array extended with none, should get property default value + assert obj.ReadProperty('location', 2) == "" + + # wrong datatype + with self.assertRaises(InvalidParameterDatatype): + obj.WriteProperty('location', "nope", arrayIndex=0) + + def test_changing_item(self): + if _debug: TestWritableArray._debug("test_changing_item") + + # create an object with a location + obj = SampleWritableArray(location=ArrayOfCharacterString(["home"])) + if _debug: TestWritableArray._debug(" - obj.location: %r", obj.location) + + # change the element + obj.WriteProperty('location', "work", arrayIndex=1) + assert obj.ReadProperty('location', 1) == "work" + + # wrong datatype + with self.assertRaises(InvalidParameterDatatype): + obj.WriteProperty('location', 12, arrayIndex=1) + + def test_replacing_array(self): + if _debug: TestWritableArray._debug("test_replacing_array") + + # create an object with a location + obj = SampleWritableArray() + if _debug: TestWritableArray._debug(" - obj.location: %r", obj.location) + + # replace the array + obj.WriteProperty('location', ArrayOfCharacterString(["home", "work"])) + assert obj.ReadProperty('location', 0) == 2 + assert obj.ReadProperty('location', 1) == "home" + assert obj.ReadProperty('location', 2) == "work" + + +@bacpypes_debugging +@register_object_type(vendor_id=999) +class SampleLocationObject(CurrentPropertyListMixIn, Object): + + objectType = 'sampleLocationObject' + properties = [ + WritableProperty('location', CharacterString), + ] + + def __init__(self, **kwargs): + if _debug: SampleWritableArray._debug("__init__ %r", kwargs) + Object.__init__(self, **kwargs) + + +@bacpypes_debugging +class TestCurrentPropertyListMixIn(unittest.TestCase): + + def test_with_location(self): + if _debug: TestCurrentPropertyListMixIn._debug("test_with_location") + + # create an object without a location + obj = SampleLocationObject(location="home") + if _debug: TestCurrentPropertyListMixIn._debug(" - obj.location: %r", obj.location) + + assert obj.propertyList.value == [1, "location"] + + def test_without_location(self): + if _debug: TestCurrentPropertyListMixIn._debug("test_property_list_1") + + # create an object without a location + obj = SampleLocationObject() + if _debug: TestCurrentPropertyListMixIn._debug(" - obj.location: %r", obj.location) + + assert obj.propertyList.value == [0] + + def test_location_appears(self): + if _debug: TestCurrentPropertyListMixIn._debug("test_location_appears") + + # create an object without a location + obj = SampleLocationObject() + if _debug: TestCurrentPropertyListMixIn._debug(" - obj.location: %r", obj.location) + + # give it a location + obj.location = "away" + assert obj.propertyList.value == [1, "location"] + + def test_location_disappears(self): + if _debug: TestCurrentPropertyListMixIn._debug("test_location_disappears") + + # create an object without a location + obj = SampleLocationObject(location="home") + if _debug: TestCurrentPropertyListMixIn._debug(" - obj.location: %r", obj.location) + + # location 'removed' + obj.location = None + + assert obj.propertyList.value == [0] +