Skip to content

Commit

Permalink
Merge pull request #232 from ChristianTremblay/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
ChristianTremblay committed Nov 22, 2020
2 parents e7c4805 + 373f420 commit 3d35093
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 16 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions BAC0/core/devices/Device.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,15 +717,17 @@ 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:
raise ValueError(
"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(
Expand Down
2 changes: 1 addition & 1 deletion BAC0/core/devices/local/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion BAC0/core/devices/mixins/read_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
144 changes: 136 additions & 8 deletions BAC0/core/io/Read.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <addr> ( <type> <inst> ( <prop> [ <indx> ] )... )...
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -305,13 +311,21 @@ 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(
readResult.propertyAccessError
)
)
values.append(None)
dict_values[objectIdentifier].append((_prop_id, None))
else:
# here is the value
propertyValue = readResult.propertyValue
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
):
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion BAC0/infos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.04"
__version__ = "20.11.21"
__license__ = "LGPLv3"
5 changes: 3 additions & 2 deletions BAC0/tasks/Poll.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
1 change: 1 addition & 0 deletions BAC0/tasks/TaskManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def process(cls):
)
cls.stop_service()
cls.start_service()
time.sleep(0.01)
cls.stop_service()

@classmethod
Expand Down
3 changes: 3 additions & 0 deletions doc/source/cov.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------
Expand Down
54 changes: 54 additions & 0 deletions doc/source/read.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 ::
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from setuptools import setup
from BAC0 import infos

requirements = ["bacpypes"]
requirements = ["bacpypes", "netifaces", "colorama"]

setup(
name="BAC0",
Expand Down

0 comments on commit 3d35093

Please sign in to comment.