From 15bf0940a8852978a7e1f9d22fcb801d68ba7925 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Sun, 8 Nov 2020 20:53:32 -0500 Subject: [PATCH 1/9] New feature for readMultiple. Request_dict allow to create simple RPM requests and the result is presented as a dict for clarity --- BAC0/core/io/Read.py | 144 ++++++++++++++++++++++++++++++++++++++++--- doc/source/read.rst | 54 ++++++++++++++++ setup.py | 2 +- 3 files changed, 191 insertions(+), 9 deletions(-) diff --git a/BAC0/core/io/Read.py b/BAC0/core/io/Read.py index f5b01f6a..c7d14b00 100644 --- a/BAC0/core/io/Read.py +++ b/BAC0/core/io/Read.py @@ -232,7 +232,9 @@ def _split_the_read_request(self, args, arr_index): nmbr_obj = self.read(args, arr_index=0) return [self.read(args, arr_index=i) for i in range(1, nmbr_obj + 1)] - def readMultiple(self, args, vendor_id=0, timeout=10, prop_id_required=False): + def readMultiple( + self, args, request_dict=None, vendor_id=0, timeout=10, prop_id_required=False + ): """ Build a ReadPropertyMultiple request, wait for the answer and return the values :param args: String with ( ( [ ] )... )... @@ -251,15 +253,19 @@ def readMultiple(self, args, vendor_id=0, timeout=10, prop_id_required=False): if not self._started: raise ApplicationNotStarted("BACnet stack not running - use startApp()") - args = args.split() - vendor_id = vendor_id - values = [] + if request_dict is not None: + request = self.build_rpm_request_from_dict(request_dict, vendor_id) + else: + args = args.split() + request = self.build_rpm_request(args, vendor_id=vendor_id) + self.log_title("Read Multiple", args) - self.log_title("Read Multiple", args) + values = [] + dict_values = {} try: # build an ReadPropertyMultiple request - iocb = IOCB(self.build_rpm_request(args, vendor_id=vendor_id)) + iocb = IOCB(request) iocb.set_timeout(timeout) # pass to the BACnet stack deferred(self.this_application.request_io, iocb) @@ -296,7 +302,7 @@ def readMultiple(self, args, vendor_id=0, timeout=10, prop_id_required=False): ) ) self._log.debug("-" * 114) - + dict_values[objectIdentifier] = [] # now come the property values per object for element in result.listOfResults: # get the property and array index @@ -305,6 +311,13 @@ def readMultiple(self, args, vendor_id=0, timeout=10, prop_id_required=False): readResult = element.readResult + if propertyArrayIndex is not None: + _prop_id = "{}@idx:{}".format( + propertyIdentifier, propertyArrayIndex + ) + else: + _prop_id = propertyIdentifier + if readResult.propertyAccessError is not None: self._log.debug( "Property Access Error for {}".format( @@ -312,6 +325,7 @@ def readMultiple(self, args, vendor_id=0, timeout=10, prop_id_required=False): ) ) values.append(None) + dict_values[objectIdentifier].append((_prop_id, None)) else: # here is the value propertyValue = readResult.propertyValue @@ -353,10 +367,17 @@ def readMultiple(self, args, vendor_id=0, timeout=10, prop_id_required=False): except ValueError: prop_id = propertyIdentifier values.append((value, prop_id)) + dict_values[objectIdentifier].append( + (_prop_id, (value, prop_id)) + ) else: values.append(value) + dict_values[objectIdentifier].append((_prop_id, value)) - return values + if request_dict is not None: + return dict_values + else: + return values if iocb.ioError: # unsuccessful: error/reject/abort apdu = iocb.ioError @@ -510,6 +531,53 @@ def build_rpm_request(self, args, vendor_id=0): request.pduDestination = Address(addr) return request + def build_rpm_request_from_dict(self, request_dict, vendor_id): + """ + Read property multiple allow to read a lot of properties with only one request + The existing RPM function is made using a string that must be created using bacpypes + console style and is hard to automate. + + This new version will be an attempt to improve that:: + + _rpm = {'address': '11:2', + 'objects': {'analogInput:1': ['presentValue', 'description', 'unit', 'objectList@idx:0'], + 'analogInput:2': ['presentValue', 'description', 'unit', 'objectList@idx:0'], + }, + vendor_id: 842 + } + + """ + vendor_id = 842 + addr = request_dict["address"] + objects = request_dict["objects"] + if "vendor_id" in request_dict.keys(): + vendor_id = int(request_dict["vendor_id"]) + + read_access_spec_list = [] + + for obj, list_of_properties in objects.items(): + obj_type, obj_instance = obj.split(":") + obj_type = validate_object_type(obj_type, vendor_id=vendor_id) + obj_instance = int(obj_instance) + property_reference_list = build_property_reference_list( + obj_type, list_of_properties + ) + read_acces_spec = build_read_access_spec( + obj_type, obj_instance, property_reference_list + ) + read_access_spec_list.append(read_acces_spec) + + if not read_access_spec_list: + raise RuntimeError("at least one read access specification required") + + # build the request + request = ReadPropertyMultipleRequest( + listOfReadAccessSpecs=read_access_spec_list + ) + request.pduDestination = Address(addr) + + return request + def build_rrange_request( self, args, range_params=None, arr_index=None, vendor_id=0, bacoid=None ): @@ -740,3 +808,63 @@ def cast_datatype_from_tag(propertyValue, obj_id, prop_id): except: value = {"{}_{}".format(obj_id, prop_id): propertyValue} return value + + +def validate_object_type(obj_type, vendor_id=842): + if obj_type.isdigit(): + obj_type = int(obj_type) + elif "@obj_" in obj_type: + obj_type = int(obj_type.split("_")[1]) + elif not get_object_class(obj_type, vendor_id=vendor_id): + raise ValueError("Unknown object type : {}".format(obj_type)) + return obj_type + + +def build_read_access_spec(obj_type, obj_instance, property_reference_list): + return ReadAccessSpecification( + objectIdentifier=(obj_type, obj_instance), + listOfPropertyReferences=property_reference_list, + ) + + +def build_property_reference_list(obj_type, list_of_properties): + property_reference_list = [] + for prop in list_of_properties: + idx = None + if "@idx:" in prop: + prop, idx = prop.split("@idx:") + prop_id = validate_property_id(obj_type, prop) + prop_reference = PropertyReference(propertyIdentifier=prop_id) + if idx: + prop_reference.propertyArrayIndex = int(idx) + property_reference_list.append(prop_reference) + return property_reference_list + + +def validate_property_id(obj_type, prop_id): + if prop_id in PropertyIdentifier.enumerations: + if prop_id in ( + "all", + "required", + "optional", + "objectName", + "objectType", + "objectIdentifier", + "polarity", + ): + return prop_id + elif validate_datatype(obj_type, prop_id) is not None: + return prop_id + else: + raise ValueError( + "invalid property for object type : {} | {}".format(obj_type, prop_id) + ) + elif "@prop_" in prop_id: + return int(prop_id.split("_")[1]) + # elif "@obj_" in prop_id: + else: + raise ValueError("{} is an invalid property for {}".format(prop_id, obj_type)) + + +def validate_datatype(obj_type, prop_id, vendor_id=842): + return get_datatype(obj_type, prop_id, vendor_id=vendor_id) if not None else False diff --git a/doc/source/read.rst b/doc/source/read.rst index 04dab81a..158ab80a 100644 --- a/doc/source/read.rst +++ b/doc/source/read.rst @@ -88,6 +88,60 @@ Read property multiple can also be used :: bacnet.readMultiple('address object object_instance property_1 property_2') #or bacnet.readMultiple('address object object_instance all') +Read multiple +.................. +Using simple read is a costly way of retrieving data. If you need to read a lot of data from a controller, +and this controller supports read multiple, you should use that feature. + +When defining `BAC0.devices`, all polling requests will use readMultiple to retrive the information on the network. + +There is actually two way of defining a read multiple request. The first one inherit from bacpypes console examples +and is based on a string composed from a list of properties to be read on the network. This is the example I showed +previously. + +Recently, a more flexible way of creating those requests have been added using a dict to create the requests. +The results are then provided as a dict for clarity. Because the old way just give all the result in order of the request, +which can lead to some errors and is very hard to interact with on the REPL. + +The `request_dict` must be created like this :: + + _rpm = {'address': '303:9', + 'objects': { + 'analogInput:1094': ['objectName', 'presentValue', 'statusFlags', 'units','description'], + 'analogValue:4410': ['objectName', 'presentValue', 'statusFlags', 'units', 'description'] + } + } + +If an array index needs to be used, the following syntax can be used in the property name :: + + # Array index 1 of propertyName + 'propertyName@idx:1' + +This dict must be used with the already exsiting function `bacnet.readMultiple()` and passed +via the argument named **request_dict**. :: + + bacnet.readMultiple('303:9', request_dict=_rpm) + +The result will be a dict containing all the information requested. :: + + # result of request + { + ('analogInput', 1094): [ + ('objectName', 'DA-VP'), + ('presentValue', 4.233697891235352), + ('statusFlags', [0, 0, 0, 0]), + ('units', 'pascals'), + ('description', 'Discharge Air Velocity Pressure') + ], + ('analogValue', 4410): [ + ('objectName', 'SAFLOW-ABSEFFORT'), + ('presentValue', 0.005016503389924765), + ('statusFlags', [0, 0, 1, 0]), + ('units', 'percent'), + ('description', '') + ] + } + Write to property ........................ To write to a single property :: diff --git a/setup.py b/setup.py index c25a5f33..dcf527b7 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from BAC0 import infos -requirements = ["bacpypes"] +requirements = ["bacpypes", "netifaces", "colorama"] setup( name="BAC0", From d1b7588efe51a538be5d9ef01f8451a31da75dbb Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Sun, 8 Nov 2020 21:24:37 -0500 Subject: [PATCH 2/9] Adding colorama to Travis... looks like it needs it --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index c925a09b..09a029e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ bundler_args: "--retry 3" install: - pip install -r requirements.txt - pip install coveralls +- pip install colorama - pip install pytest - pip install pytest-cov - pip install pandas From ded7e93fa6e799dbf7b7b9f15bd44f8be56fc4b4 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Sun, 8 Nov 2020 21:51:34 -0500 Subject: [PATCH 3/9] Using write_property with other properties than presentValue do not necessarely needs priority... now default value is None --- BAC0/core/devices/Device.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/BAC0/core/devices/Device.py b/BAC0/core/devices/Device.py index 55758091..321db95c 100755 --- a/BAC0/core/devices/Device.py +++ b/BAC0/core/devices/Device.py @@ -717,7 +717,9 @@ def read_property(self, prop): raise Exception("Unknown property : {}".format(error)) return val - def write_property(self, prop, value, priority=16): + def write_property(self, prop, value, priority=None): + if priority is not None: + priority = "- {}".format(priority) if isinstance(prop, tuple): _obj, _instance, _prop = prop else: @@ -725,7 +727,7 @@ def write_property(self, prop, value, priority=16): "Please provide property using tuple with object, instance and property" ) try: - request = "{} {} {} {} {} - {}".format( + request = "{} {} {} {} {} {}".format( self.properties.address, _obj, _instance, _prop, value, priority ) val = self.properties.network.write( From 5478d16b81adf88176c46711782d20535d08dd54 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Fri, 13 Nov 2020 23:52:44 -0500 Subject: [PATCH 4/9] Fix bug with local object description --- BAC0/core/devices/local/decorator.py | 2 +- BAC0/infos.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BAC0/core/devices/local/decorator.py b/BAC0/core/devices/local/decorator.py index 00af0526..dc4eb987 100644 --- a/BAC0/core/devices/local/decorator.py +++ b/BAC0/core/devices/local/decorator.py @@ -229,6 +229,6 @@ def create(object_type, instance, objectName, presentValue, description): objectIdentifier=(object_type.objectType, instance), objectName="{}".format(objectName), presentValue=presentValue, - description=CharacterString("{}".format(presentValue)), + description=CharacterString("{}".format(description)), ) return new_object diff --git a/BAC0/infos.py b/BAC0/infos.py index 4908519a..e8f5b1af 100644 --- a/BAC0/infos.py +++ b/BAC0/infos.py @@ -12,5 +12,5 @@ __email__ = "christian.tremblay@servisys.com" __url__ = "https://github.com/ChristianTremblay/BAC0" __download_url__ = "https://github.com/ChristianTremblay/BAC0/archive/master.zip" -__version__ = "20.11.04dev" +__version__ = "20.11.13dev" __license__ = "LGPLv3" From 7ca1f08b1c46a08ea291c3041685c2e4b32853c5 Mon Sep 17 00:00:00 2001 From: Nathan Merritt Date: Tue, 17 Nov 2020 16:06:20 -0700 Subject: [PATCH 5/9] add exception string to error log when polling fails --- BAC0/tasks/Poll.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/BAC0/tasks/Poll.py b/BAC0/tasks/Poll.py index 9b403218..384daf60 100644 --- a/BAC0/tasks/Poll.py +++ b/BAC0/tasks/Poll.py @@ -94,9 +94,10 @@ def task(self): # When creation fail, polling is created and fail the first time... # So kill the task self.stop() - except ValueError: + except ValueError as e: self.device._log.error( - "Something is wrong with polling...stopping. Try setting off segmentation" + "Something is wrong with polling...stopping. Try setting off " + "segmentation. Error: {}".format(e) ) self.stop() From aada604dbfe9a6f4176ed6bf705b2ac41897ae55 Mon Sep 17 00:00:00 2001 From: Nathan Merritt Date: Tue, 17 Nov 2020 16:14:27 -0700 Subject: [PATCH 6/9] make ReadProperty::read_multiple accept point names --- BAC0/core/devices/mixins/read_mixin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BAC0/core/devices/mixins/read_mixin.py b/BAC0/core/devices/mixins/read_mixin.py index b2bcdebb..22860016 100755 --- a/BAC0/core/devices/mixins/read_mixin.py +++ b/BAC0/core/devices/mixins/read_mixin.py @@ -529,7 +529,8 @@ def read_multiple( device.read_multiple(['point1', 'point2', 'point3'], points_per_request = 10) """ if isinstance(points_list, list): - for each in points_list: + (requests, _) = self._rpm_request_by_name(points_list) + for each in requests: self.read_single( each, points_per_request=1, discover_request=discover_request ) From 91614136540c2b39abf454e59fe21df959c563ef Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Sat, 21 Nov 2020 20:31:31 -0500 Subject: [PATCH 7/9] A missing time.sleep caused CPU to go to 100% on Linux... weird, Windows was not having this bug... or at least not as strong... --- BAC0/tasks/TaskManager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/BAC0/tasks/TaskManager.py b/BAC0/tasks/TaskManager.py index b637ef65..e760a85b 100644 --- a/BAC0/tasks/TaskManager.py +++ b/BAC0/tasks/TaskManager.py @@ -77,6 +77,7 @@ def process(cls): ) cls.stop_service() cls.start_service() + time.sleep(0.01) cls.stop_service() @classmethod From 7e50f39cdf4cfc265a6c650baa83607ed30e2ad8 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Sat, 21 Nov 2020 20:32:29 -0500 Subject: [PATCH 8/9] Details on objectID for cov --- doc/source/cov.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/source/cov.rst b/doc/source/cov.rst index 76a015a2..66dd68b2 100644 --- a/doc/source/cov.rst +++ b/doc/source/cov.rst @@ -12,6 +12,9 @@ or from the network itself :: bacnet.cov(address, objectID) +.. note:: + objectID is a tuple created with the object type as a string and the instance. For example + analog input 1 would be : `("analogInput", 1)` Confirmed COV -------------- From 373f420b20cb5e0e1c51b17a7f7009df387730e8 Mon Sep 17 00:00:00 2001 From: Christian Tremblay Date: Sat, 21 Nov 2020 20:36:15 -0500 Subject: [PATCH 9/9] Update infos.py Bumping version for hot fix release --- BAC0/infos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BAC0/infos.py b/BAC0/infos.py index 88fb30ee..793ec038 100644 --- a/BAC0/infos.py +++ b/BAC0/infos.py @@ -12,5 +12,5 @@ __email__ = "christian.tremblay@servisys.com" __url__ = "https://github.com/ChristianTremblay/BAC0" __download_url__ = "https://github.com/ChristianTremblay/BAC0/archive/master.zip" -__version__ = "20.11.17dev" +__version__ = "20.11.21" __license__ = "LGPLv3"