From ff6371464706f7c25bfe73e9980cf83ae4f8b901 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Fri, 21 Feb 2020 15:23:23 -0500 Subject: [PATCH 01/27] Added bacnet.read_priority_array() as a shortcut to have all the result easily --- BAC0/core/io/Read.py | 11 +++++++++++ BAC0/infos.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/BAC0/core/io/Read.py b/BAC0/core/io/Read.py index 6e39080e..fe2fc4a1 100644 --- a/BAC0/core/io/Read.py +++ b/BAC0/core/io/Read.py @@ -632,6 +632,17 @@ def readRange(self, args, arr_index=None, vendor_id=0, bacoid=None, timeout=10): "APDU Abort Reason : {}".format(reason) ) + def read_priority_array(self, addr, obj, obj_instance): + pa = self.read("{} {} {} priorityArray".format(addr, obj, obj_instance)) + res = [] + res.append(pa) + for each in range(1, 17): + _pa = pa[each] + for k, v in _pa.__dict__.items(): + if v != None: + res.append(v) + return res + def find_reason(apdu): try: diff --git a/BAC0/infos.py b/BAC0/infos.py index 68a34576..cffb2db0 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.02.20" +__version__ = "20.02.21dev" __license__ = "LGPLv3" From c04df79ec8b7ddc2e3a65494c4586932378fceac Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Mon, 11 May 2020 09:43:47 -0400 Subject: [PATCH 02/27] Added DeviceCommunicationControl functionality as requested per issue #197. I have no way to test. USer will tell if it works --- .gitignore | 4 + .../functions/DeviceCommunicationContol.py | 95 +++++++++++++++++++ BAC0/scripts/Lite.py | 10 +- 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 BAC0/core/functions/DeviceCommunicationContol.py diff --git a/.gitignore b/.gitignore index 2595c547..683c58b4 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,7 @@ obj30.bin obj30.db obj300.bin /.vscode +*.bin +*.db +*.log +wily-report.html diff --git a/BAC0/core/functions/DeviceCommunicationContol.py b/BAC0/core/functions/DeviceCommunicationContol.py new file mode 100644 index 00000000..05abf71f --- /dev/null +++ b/BAC0/core/functions/DeviceCommunicationContol.py @@ -0,0 +1,95 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 by Christian Tremblay, P.Eng +# Licensed under LGPLv3, see file LICENSE in this source tree. +# +""" +Reinitialize.py - creation of ReinitializeDeviceRequest + +""" +from ...core.io.Read import find_reason +from ..io.IOExceptions import ( + SegmentationNotSupported, + ReadPropertyException, + ReadPropertyMultipleException, + NoResponseFromController, + ApplicationNotStarted, +) +from bacpypes.object import Unsigned16 +from ...core.utils.notes import note_and_log + +# --- standard Python modules --- +import datetime as dt + +# --- 3rd party modules --- +from bacpypes.pdu import Address, GlobalBroadcast +from bacpypes.primitivedata import Date, Time, CharacterString +from bacpypes.basetypes import DateTime +from bacpypes.apdu import ( + DeviceCommunicationControlRequest, + DeviceCommunicationControlRequestEnableDisable, + SimpleAckPDU, +) +from bacpypes.iocb import IOCB +from bacpypes.core import deferred + + +@note_and_log +class DeviceCommunicationControl: + """ + Mixin to support DeviceCommunicationControl from BAC0 to other devices + """ + + def dcc(self, address=None, duration=None, password=None, state=None): + """ + Will send DeviceCommunicationControl request + """ + if not self._started: + raise ApplicationNotStarted("BACnet stack not running - use startApp()") + + if not address: + raise ValueError("Provide address for request") + + if not state: + raise ValueError("Provide state ('enable', 'disable', 'disableInitiation'") + + # build a request + request = DeviceCommunicationControlRequest() + request.enableDisable = DeviceCommunicationControlRequestEnableDisable.enumerations[ + state + ] + request.pduDestination = Address(address) + if duration: + request.duration = Unsigned16(duration) + + request.password = CharacterString(password) + + self._log.debug("{:>12} {}".format("- request:", request)) + + iocb = IOCB(request) # make an IOCB + + # pass to the BACnet stack + deferred(self.this_application.request_io, iocb) + + # Unconfirmed request...so wait until complete + iocb.wait() # Wait for BACnet response + + if iocb.ioResponse: # successful response + apdu = iocb.ioResponse + + if not isinstance(iocb.ioResponse, SimpleAckPDU): # expect an ACK + self._log.warning("Not an ack, see debug for more infos.") + self._log.debug( + "Not an ack. | APDU : {} / {}".format((apdu, type(apdu))) + ) + return + + if iocb.ioError: # unsuccessful: error/reject/abort + apdu = iocb.ioError + reason = find_reason(apdu) + raise NoResponseFromController("APDU Abort Reason : {}".format(reason)) + + self._log.info( + "DeviceCommunicationControl request sent to device : {}".format(address) + ) diff --git a/BAC0/scripts/Lite.py b/BAC0/scripts/Lite.py index 79bf15fc..f32b095f 100755 --- a/BAC0/scripts/Lite.py +++ b/BAC0/scripts/Lite.py @@ -36,6 +36,7 @@ class ReadWriteScript(BasicScript,ReadProperty,WriteProperty) from ..core.functions.Discover import Discover from ..core.functions.TimeSync import TimeSync from ..core.functions.Reinitialize import Reinitialize +from ..core.functions.DeviceCommunicationControl import DeviceCommunicationControl from ..core.io.Simulate import Simulation from ..core.devices.Points import Point from ..core.devices.Device import RPDeviceConnected, RPMDeviceConnected @@ -57,7 +58,14 @@ class ReadWriteScript(BasicScript,ReadProperty,WriteProperty) @note_and_log class Lite( - Base, Discover, ReadProperty, WriteProperty, Simulation, TimeSync, Reinitialize + Base, + Discover, + ReadProperty, + WriteProperty, + Simulation, + TimeSync, + Reinitialize, + DeviceCommunicationControl, ): """ Build a BACnet application to accept read and write requests. From c713d755f7cd013975e1b48c1fec1fb55285a9d5 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Tue, 12 May 2020 09:13:32 -0400 Subject: [PATCH 03/27] Being evil with log when silence mode is chosen as proposed in issue #195 --- BAC0/core/utils/notes.py | 7 +++++++ BAC0/infos.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/BAC0/core/utils/notes.py b/BAC0/core/utils/notes.py index 5a7cfa16..c4855792 100644 --- a/BAC0/core/utils/notes.py +++ b/BAC0/core/utils/notes.py @@ -92,6 +92,7 @@ def update_log_level( update_log_file_lvl = True update_stderr_lvl = True update_stdout_lvl = True + logging.getLogger("BAC0_Root.BAC0.scripts.Base.Base").disabled = True elif level.lower() == "default": log_file_lvl = logging.WARNING stderr_lvl = logging.CRITICAL @@ -110,6 +111,12 @@ def update_log_level( stdout_lvl = level update_log_file_lvl = True update_stdout_lvl = True + + if ( + level.lower() != "silence" + and logging.getLogger("BAC0_Root.BAC0.scripts.Base.Base").disabled + ): + logging.getLogger("BAC0_Root.BAC0.scripts.Base.Base").disabled = False else: if log_file: log_file_lvl = convert_level(log_file) diff --git a/BAC0/infos.py b/BAC0/infos.py index cffb2db0..02e87065 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.02.21dev" +__version__ = "20.05.12dev" __license__ = "LGPLv3" From e163cd0da4f4c1978ba2366e970d6cdcd87b2423 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Wed, 13 May 2020 16:08:37 -0400 Subject: [PATCH 04/27] Typo in module DeviceCommunicationControl --- ...DeviceCommunicationContol.py => DeviceCommunicationControl.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename BAC0/core/functions/{DeviceCommunicationContol.py => DeviceCommunicationControl.py} (100%) diff --git a/BAC0/core/functions/DeviceCommunicationContol.py b/BAC0/core/functions/DeviceCommunicationControl.py similarity index 100% rename from BAC0/core/functions/DeviceCommunicationContol.py rename to BAC0/core/functions/DeviceCommunicationControl.py From 0744f6d5817fd9c709e9b41400e926c0e62fae86 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Mon, 18 May 2020 20:19:29 -0400 Subject: [PATCH 05/27] Fixing bug reported in issue #196 when a Device ID of 0 isn't recognized --- BAC0/core/devices/Device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BAC0/core/devices/Device.py b/BAC0/core/devices/Device.py index 9012fc56..3329627b 100755 --- a/BAC0/core/devices/Device.py +++ b/BAC0/core/devices/Device.py @@ -186,7 +186,7 @@ def __init__( if ( self.properties.network and self.properties.address - and self.properties.device_id + and self.properties.device_id is not None ): self.new_state(DeviceDisconnected) else: From 5d951862e5a7b7484740e30704c1eb30fc7eabe1 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Sun, 24 May 2020 22:30:00 -0400 Subject: [PATCH 06/27] Discover was not handling correctly the case where there is no BACnet router on the network, or no device could tell on what network we are. Ref. issue #199 --- BAC0/scripts/Lite.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/BAC0/scripts/Lite.py b/BAC0/scripts/Lite.py index f32b095f..16982d38 100755 --- a/BAC0/scripts/Lite.py +++ b/BAC0/scripts/Lite.py @@ -209,13 +209,15 @@ def discover( if isinstance(networks, list): # we'll make multiple whois... for network in networks: - _networks.append(network) + if networks < 65535: + _networks.append(network) elif networks == "known": _networks = self.known_network_numbers else: if networks < 65535: _networks.append(networks) + if _networks: for network in _networks: self._log.info("Discovering network {}".format(network)) _res = self.whois( @@ -230,6 +232,11 @@ def discover( found.append(each) else: + self._log.info( + "No BACnet network found, attempting a simple whois using provided device instances limits ({} - {})".format( + deviceInstanceRangeLowLimit, deviceInstanceRangeHighLimit + ) + ) _res = self.whois( "{} {}".format( deviceInstanceRangeLowLimit, deviceInstanceRangeHighLimit From a2d1ed6dd47250387eff118202919aaada239482 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Sun, 24 May 2020 22:32:36 -0400 Subject: [PATCH 07/27] Fix issue with proprietary properties write --- BAC0/core/io/Write.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BAC0/core/io/Write.py b/BAC0/core/io/Write.py index 49b79f84..00046a4f 100644 --- a/BAC0/core/io/Write.py +++ b/BAC0/core/io/Write.py @@ -134,7 +134,7 @@ def _parse_wp_args(self, args): if len(args) >= 6: priority = int(args[5]) if "@prop_" in prop_id: - prop_id = int(prop_id.split("_")[1]) + prop_id = prop_id.split("_")[1] if prop_id.isdigit(): prop_id = int(prop_id) From b91045a7a4b68840ce65b3e74e3fd7fa40ae3084 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Sun, 24 May 2020 22:34:16 -0400 Subject: [PATCH 08/27] Adding modeltype and state of JCI device proprietray properties --- BAC0/core/proprietary_objects/jci.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/BAC0/core/proprietary_objects/jci.py b/BAC0/core/proprietary_objects/jci.py index f28c0296..a41aae07 100644 --- a/BAC0/core/proprietary_objects/jci.py +++ b/BAC0/core/proprietary_objects/jci.py @@ -33,7 +33,9 @@ "bacpypes_type": DeviceObject, "properties": { "SupervisorOnline": {"obj_id": 3653, "primitive": Boolean, "mutable": True}, - "Model": {"obj_id": 1320, "primitive": CharacterString, "mutable": False}, + "Model": {"obj_id": 1320, "primitive": CharacterString, "mutable": True}, + "ModelType": {"obj_id": 32527, "primitive": CharacterString, "mutable": True}, + "State": {"obj_id": 2390, "primitive": CharacterString, "mutable": False}, "MemoryUsage": {"obj_id": 2581, "primitive": Real, "mutable": False}, "ObjectMemoryUsage": {"obj_id": 2582, "primitive": Real, "mutable": False}, "CPU": {"obj_id": 2583, "primitive": Real, "mutable": False}, From b11d561744ad606466aeaa709db88b5cde021daf Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Sun, 24 May 2020 22:35:18 -0400 Subject: [PATCH 09/27] Bumping dev version --- BAC0/infos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BAC0/infos.py b/BAC0/infos.py index 02e87065..7849bead 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.05.12dev" +__version__ = "20.05.24dev" __license__ = "LGPLv3" From 61b7ce7f08ff19145d18381130bcb6c96bdf008a Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Thu, 11 Jun 2020 21:05:19 -0400 Subject: [PATCH 10/27] This commit tries to fix issue #201 if a device gives a bufferoverflow error when reading the object list. Also a typo fix in network validation --- BAC0/core/devices/mixins/read_mixin.py | 3 ++- BAC0/core/io/IOExceptions.py | 8 ++++++++ BAC0/core/io/Read.py | 8 ++++++++ BAC0/scripts/Lite.py | 2 +- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/BAC0/core/devices/mixins/read_mixin.py b/BAC0/core/devices/mixins/read_mixin.py index 9f13dc3f..72e07b99 100755 --- a/BAC0/core/devices/mixins/read_mixin.py +++ b/BAC0/core/devices/mixins/read_mixin.py @@ -17,6 +17,7 @@ ReadPropertyMultipleException, NoResponseFromController, SegmentationNotSupported, + BufferOverflow, ) from ..Points import NumericPoint, BooleanPoint, EnumPoint, StringPoint, OfflinePoint from ..Trends import TrendLog @@ -239,7 +240,7 @@ def _discoverPoints(self, custom_object_list=None): ) objList = [] - except SegmentationNotSupported: + except (SegmentationNotSupported, BufferOverflow): objList = [] number_of_objects = self.properties.network.read( "{} device {} objectList".format( diff --git a/BAC0/core/io/IOExceptions.py b/BAC0/core/io/IOExceptions.py index b8dd715a..29174207 100644 --- a/BAC0/core/io/IOExceptions.py +++ b/BAC0/core/io/IOExceptions.py @@ -150,3 +150,11 @@ class RemovedPointException(Exception): """ pass + + +class BufferOverflow(Exception): + """ + Buffer capacity of device exceeded. + """ + + pass diff --git a/BAC0/core/io/Read.py b/BAC0/core/io/Read.py index fe2fc4a1..a021ff7b 100644 --- a/BAC0/core/io/Read.py +++ b/BAC0/core/io/Read.py @@ -61,6 +61,7 @@ def readMultiple() SegmentationNotSupported, UnknownPropertyError, UnknownObjectError, + BufferOverflow, ) from ..utils.notes import note_and_log @@ -192,6 +193,13 @@ def read( elif reason == "unknownObject": self._log.warning("Unknown object {}".format(args)) raise UnknownObjectError("Unknown object {}".format(args)) + elif reason == "bufferOverflow": + self._log.warning( + "Buffer capacity exceeded in device{}".format(args) + ) + raise BufferOverflow( + "Buffer capacity exceeded in device{}".format(args) + ) else: # Other error... consider NoResponseFromController (65) # even if the real reason is another one diff --git a/BAC0/scripts/Lite.py b/BAC0/scripts/Lite.py index 16982d38..3c1e561f 100755 --- a/BAC0/scripts/Lite.py +++ b/BAC0/scripts/Lite.py @@ -209,7 +209,7 @@ def discover( if isinstance(networks, list): # we'll make multiple whois... for network in networks: - if networks < 65535: + if network < 65535: _networks.append(network) elif networks == "known": _networks = self.known_network_numbers From 585db36bf623582fff966b2cc4ee6cc8d126f077 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Thu, 18 Jun 2020 20:47:45 -0400 Subject: [PATCH 11/27] import issue as found in issue #201 --- .../functions/DeviceCommunicationControl.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/BAC0/core/functions/DeviceCommunicationControl.py b/BAC0/core/functions/DeviceCommunicationControl.py index 05abf71f..2d106c5b 100644 --- a/BAC0/core/functions/DeviceCommunicationControl.py +++ b/BAC0/core/functions/DeviceCommunicationControl.py @@ -8,17 +8,6 @@ Reinitialize.py - creation of ReinitializeDeviceRequest """ -from ...core.io.Read import find_reason -from ..io.IOExceptions import ( - SegmentationNotSupported, - ReadPropertyException, - ReadPropertyMultipleException, - NoResponseFromController, - ApplicationNotStarted, -) -from bacpypes.object import Unsigned16 -from ...core.utils.notes import note_and_log - # --- standard Python modules --- import datetime as dt @@ -33,6 +22,18 @@ ) from bacpypes.iocb import IOCB from bacpypes.core import deferred +from bacpypes.primitivedata import Unsigned16 + +from ...core.io.Read import find_reason +from ..io.IOExceptions import ( + SegmentationNotSupported, + ReadPropertyException, + ReadPropertyMultipleException, + NoResponseFromController, + ApplicationNotStarted, +) + +from ...core.utils.notes import note_and_log @note_and_log From de04bfa5c331332a204d42a13670e160e1dac91e Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Thu, 18 Jun 2020 20:59:01 -0400 Subject: [PATCH 12/27] Multiple line to import primittivedata...let's be concise. --- BAC0/core/functions/DeviceCommunicationControl.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/BAC0/core/functions/DeviceCommunicationControl.py b/BAC0/core/functions/DeviceCommunicationControl.py index 2d106c5b..cae73afd 100644 --- a/BAC0/core/functions/DeviceCommunicationControl.py +++ b/BAC0/core/functions/DeviceCommunicationControl.py @@ -13,7 +13,7 @@ # --- 3rd party modules --- from bacpypes.pdu import Address, GlobalBroadcast -from bacpypes.primitivedata import Date, Time, CharacterString +from bacpypes.primitivedata import Date, Time, CharacterString, Unsigned16 from bacpypes.basetypes import DateTime from bacpypes.apdu import ( DeviceCommunicationControlRequest, @@ -22,7 +22,6 @@ ) from bacpypes.iocb import IOCB from bacpypes.core import deferred -from bacpypes.primitivedata import Unsigned16 from ...core.io.Read import find_reason from ..io.IOExceptions import ( From d0c9c5988b97a336ec56e0bd85e3c2e705eb64e0 Mon Sep 17 00:00:00 2001 From: Herman Singh Date: Fri, 19 Jun 2020 12:50:17 -0400 Subject: [PATCH 13/27] inline ternary --- BAC0/core/devices/Device.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/BAC0/core/devices/Device.py b/BAC0/core/devices/Device.py index 3329627b..3c92a538 100755 --- a/BAC0/core/devices/Device.py +++ b/BAC0/core/devices/Device.py @@ -141,10 +141,7 @@ def __init__( self.properties.device_id = device_id self.properties.network = network self.properties.pollDelay = poll - if poll < 10: - self.properties.fast_polling = True - else: - self.properties.fast_polling = False + self.properties.fast_polling = True if poll < 10 else False self.properties.name = "" self.properties.vendor_id = 0 self.properties.objects_list = [] From b5725c5729fa61b8ee3b69377c85dead49337744 Mon Sep 17 00:00:00 2001 From: Herman Singh Date: Mon, 22 Jun 2020 09:52:02 -0400 Subject: [PATCH 14/27] typo --- BAC0/core/io/Read.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BAC0/core/io/Read.py b/BAC0/core/io/Read.py index a021ff7b..8937afcb 100644 --- a/BAC0/core/io/Read.py +++ b/BAC0/core/io/Read.py @@ -211,7 +211,7 @@ def _split_the_read_request(self, args, arr_index): """ When a device doesn't support segmentation, this function will split the request according to the length of the - predicted result which can be known when readin the array_index + predicted result which can be known when reading the array_index number 0. This can be a very long process as some devices count a large From bbe172909b149f0a7fa6206d61ef3f08abf609f0 Mon Sep 17 00:00:00 2001 From: Herman Singh Date: Mon, 22 Jun 2020 10:46:23 -0400 Subject: [PATCH 15/27] Attempt to read individually on BufferOverflow --- BAC0/core/io/Read.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/BAC0/core/io/Read.py b/BAC0/core/io/Read.py index 8937afcb..be11a78c 100644 --- a/BAC0/core/io/Read.py +++ b/BAC0/core/io/Read.py @@ -195,11 +195,9 @@ def read( raise UnknownObjectError("Unknown object {}".format(args)) elif reason == "bufferOverflow": self._log.warning( - "Buffer capacity exceeded in device{}".format(args) - ) - raise BufferOverflow( - "Buffer capacity exceeded in device{}".format(args) + "Buffer capacity exceeded in device {}".format(args) ) + return self._split_the_read_request(args, arr_index) else: # Other error... consider NoResponseFromController (65) # even if the real reason is another one From a36b930bc6b052e2f9791f6965cfef81995df29f Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Wed, 8 Jul 2020 14:15:13 -0400 Subject: [PATCH 16/27] Trying to add feature asekd in issue #208 : adding a way to choose the resampling frequency of the dataframe when saving to database --- BAC0/infos.py | 2 +- BAC0/sql/sql.py | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/BAC0/infos.py b/BAC0/infos.py index 7849bead..b7b6a05a 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.05.24dev" +__version__ = "20.07.08dev" __license__ = "LGPLv3" diff --git a/BAC0/sql/sql.py b/BAC0/sql/sql.py index c5e0ca4d..72721382 100644 --- a/BAC0/sql/sql.py +++ b/BAC0/sql/sql.py @@ -72,11 +72,17 @@ def points_properties_df(self): df = pd.DataFrame(pprops) return df - def backup_histories_df(self): + def backup_histories_df(self, resampling="1s"): """ Build a dataframe of the point histories """ backup = {} + if isinstance(resampling, str): + resampling_needed = True + resampling_freq = resampling + elif resampling == 0 or resampling == False: + resampling_needed = False + for point in self.points: try: if point.history.dtypes == object: @@ -93,7 +99,12 @@ def backup_histories_df(self): ) else: try: - backup[point.properties.name] = point.history.resample("1s").mean() + if resampling_needed: + backup[point.properties.name] = point.history.resample( + resampling_freq + ).mean() + else: + backup[point.properties.name] = point.history except: # probably not enough point... backup[point.properties.name] = point.history @@ -101,10 +112,12 @@ def backup_histories_df(self): df = pd.DataFrame(dict([(k, pd.Series(v)) for k, v in backup.items()])) return df.fillna(method="ffill") - def save(self, filename=None): + def save(self, filename=None, resampling="1s"): """ Save the point histories to sqlite3 database. Save the device object properties to a pickle file so the device can be reloaded. + + Resampling : valid Pandas resampling frequency. If 0 or False, dataframe will not be resampled on save. """ if filename: if ".db" in filename: @@ -123,7 +136,7 @@ def save(self, filename=None): last = his.index[-1] df_to_backup = self.backup_histories_df()[last:] except IndexError: - df_to_backup = self.backup_histories_df() + df_to_backup = self.backup_histories_df(resampling=resampling) else: self._log.debug("Creating a new backup database") From 4657d604571f2b3aae898f73d45c2d66f6ee1506 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Sat, 18 Jul 2020 18:54:23 -0400 Subject: [PATCH 17/27] Playing with sourcery to make my code better --- BAC0/core/app/ScriptApplication.py | 20 +++---- BAC0/core/devices/Device.py | 13 +++-- BAC0/core/devices/Points.py | 18 +++--- BAC0/core/devices/Trends.py | 55 +++++++++---------- BAC0/core/devices/create_objects.py | 3 +- BAC0/core/devices/mixins/read_mixin.py | 2 +- .../functions/DeviceCommunicationControl.py | 2 +- BAC0/core/functions/Discover.py | 6 +- BAC0/core/functions/Reinitialize.py | 2 +- BAC0/core/functions/TimeSync.py | 4 +- BAC0/core/io/Read.py | 16 ++---- BAC0/core/io/Write.py | 24 ++++---- BAC0/core/proprietary_objects/jci.py | 3 +- BAC0/core/proprietary_objects/object.py | 7 ++- BAC0/infos.py | 2 +- BAC0/scripts/Complete.py | 2 +- BAC0/scripts/Lite.py | 11 +--- BAC0/sql/sql.py | 31 ++++++----- BAC0/tasks/Match.py | 5 +- BAC0/tasks/TaskManager.py | 10 ++-- 20 files changed, 100 insertions(+), 136 deletions(-) diff --git a/BAC0/core/app/ScriptApplication.py b/BAC0/core/app/ScriptApplication.py index d60095b3..c402b30a 100644 --- a/BAC0/core/app/ScriptApplication.py +++ b/BAC0/core/app/ScriptApplication.py @@ -148,12 +148,10 @@ def do_WhoIsRequest(self, apdu): # count the times this has been received self.who_is_counter[key] += 1 - if low_limit is not None: - if self.localDevice.objectIdentifier[1] < low_limit: - return - if high_limit is not None: - if self.localDevice.objectIdentifier[1] > high_limit: - return + if low_limit is not None and self.localDevice.objectIdentifier[1] < low_limit: + return + if high_limit is not None and self.localDevice.objectIdentifier[1] > high_limit: + return # generate an I-Am self._log.debug("Responding to Who is by a Iam") self.iam_req.pduDestination = apdu.pduSource @@ -274,12 +272,10 @@ def do_WhoIsRequest(self, apdu): # count the times this has been received self.who_is_counter[key] += 1 - if low_limit is not None: - if self.localDevice.objectIdentifier[1] < low_limit: - return - if high_limit is not None: - if self.localDevice.objectIdentifier[1] > high_limit: - return + if low_limit is not None and self.localDevice.objectIdentifier[1] < low_limit: + return + if high_limit is not None and self.localDevice.objectIdentifier[1] > high_limit: + return # generate an I-Am self._log.debug("Responding to Who is by a Iam") self.iam_req.pduDestination = apdu.pduSource diff --git a/BAC0/core/devices/Device.py b/BAC0/core/devices/Device.py index 3c92a538..30399d5e 100755 --- a/BAC0/core/devices/Device.py +++ b/BAC0/core/devices/Device.py @@ -355,9 +355,11 @@ def find_point(self, objectType, objectAddress): Find point based on type and address """ for point in self.points: - if point.properties.type == objectType: - if float(point.properties.address) == objectAddress: - return point + if ( + point.properties.type == objectType + and float(point.properties.address) == objectAddress + ): + return point raise ValueError( "{} {} doesn't exist in controller".format(objectType, objectAddress) ) @@ -565,8 +567,7 @@ def __getitem__(self, point_name): self._log.error("{}".format(ve)) def __iter__(self): - for each in self.points: - yield each + yield from self.points def __contains__(self, value): """ @@ -971,7 +972,7 @@ def connect(self, *, network=None, from_backup=None): except NoResponseFromController: self._log.error("Unable to connect, keeping DB mode active") - elif from_backup or not network: + else: self._log.debug("Not connected, open DB") if from_backup: self.properties.db_name = from_backup.split(".")[0] diff --git a/BAC0/core/devices/Points.py b/BAC0/core/devices/Points.py index 22fda5f3..8c86575f 100644 --- a/BAC0/core/devices/Points.py +++ b/BAC0/core/devices/Points.py @@ -104,8 +104,7 @@ def __init__( self._match_task.running = False self._history.timestamp = [] - self._history.value = [] - self._history.value.append(presentValue) + self._history.value = [presentValue] self._history.timestamp.append(datetime.now()) self.history_size = history_size @@ -163,7 +162,7 @@ def read_priority_array(self): def read_property(self, prop): try: - res = self.properties.device.properties.network.read( + return self.properties.device.properties.network.read( "{} {} {} {}".format( self.properties.device.properties.address, self.properties.type, @@ -172,7 +171,6 @@ def read_property(self, prop): ), vendor_id=0, ) - return res except Exception as e: raise Exception("Problem reading : {} | {}".format(self.properties.name, e)) @@ -239,7 +237,7 @@ def priority(self, priority=None): def _trend(self, res): self._history.timestamp.append(datetime.now()) self._history.value.append(res) - if self.properties.history_size == None: + if self.properties.history_size is None: return else: if self.properties.history_size < 1: @@ -379,12 +377,10 @@ def sim(self, value, *, force=False): :param value: (float) value to simulate """ if ( - self.properties.simulated[0] - and self.properties.simulated[1] == value - and force == False + not self.properties.simulated[0] + or self.properties.simulated[1] != value + or force != False ): - pass - else: self.properties.device.properties.network.sim( "{} {} {} presentValue {}".format( self.properties.device.properties.address, @@ -720,7 +716,7 @@ def boolValue(self): """ returns : (boolean) Value """ - if self.lastValue == 1 or self.lastValue == "active": + if self.lastValue in [1, "active"]: self._key = 1 self._boolKey = True else: diff --git a/BAC0/core/devices/Trends.py b/BAC0/core/devices/Trends.py index 0edaadba..71e2883b 100755 --- a/BAC0/core/devices/Trends.py +++ b/BAC0/core/devices/Trends.py @@ -141,42 +141,37 @@ def create_dataframe(self, log_buffer): @property def history(self): - if _PANDAS: - objectType, objectAddress = ( - self.properties.log_device_object_property.objectIdentifier - ) - try: - logged_point = self.properties.device.find_point( - objectType, objectAddress - ) - except ValueError: - logged_point = None - serie = self.properties._df[self.properties.object_name].copy() - serie.units = logged_point.properties.units_state if logged_point else "n/a" - serie.name = ("{}/{}").format( - self.properties.device.properties.name, self.properties.object_name - ) - if not logged_point: - serie.states = "unknown" - else: - if logged_point.properties.name in self.properties.device.binary_states: - serie.states = "binary" - elif ( - logged_point.properties.name in self.properties.device.multi_states - ): - serie.states = "multistates" - else: - serie.states = "analog" - serie.description = self.properties.description - serie.datatype = objectType - return serie - else: + if not _PANDAS: return dict( zip( self.properties._history_components[0], self.properties._history_components[1], ) ) + objectType, objectAddress = ( + self.properties.log_device_object_property.objectIdentifier + ) + try: + logged_point = self.properties.device.find_point(objectType, objectAddress) + except ValueError: + logged_point = None + serie = self.properties._df[self.properties.object_name].copy() + serie.units = logged_point.properties.units_state if logged_point else "n/a" + serie.name = ("{}/{}").format( + self.properties.device.properties.name, self.properties.object_name + ) + if not logged_point: + serie.states = "unknown" + else: + if logged_point.properties.name in self.properties.device.binary_states: + serie.states = "binary" + elif logged_point.properties.name in self.properties.device.multi_states: + serie.states = "multistates" + else: + serie.states = "analog" + serie.description = self.properties.description + serie.datatype = objectType + return serie def chart(self, remove=False): """ diff --git a/BAC0/core/devices/create_objects.py b/BAC0/core/devices/create_objects.py index b5464622..dd607b53 100644 --- a/BAC0/core/devices/create_objects.py +++ b/BAC0/core/devices/create_objects.py @@ -87,14 +87,13 @@ def create_AI(oid=1, pv=0, name="AI", units=None): def create_BI(oid=1, pv=0, name="BI", activeText="On", inactiveText="Off"): - bio = BinaryInputObject( + return BinaryInputObject( objectIdentifier=("binaryInput", oid), objectName=name, presentValue=pv, activeText=activeText, inactiveText=inactiveText, ) - return bio def create_AO(oid=1, pv=0, name="AO", units=None, pv_writable=False): diff --git a/BAC0/core/devices/mixins/read_mixin.py b/BAC0/core/devices/mixins/read_mixin.py index 72e07b99..b2bcdebb 100755 --- a/BAC0/core/devices/mixins/read_mixin.py +++ b/BAC0/core/devices/mixins/read_mixin.py @@ -422,7 +422,7 @@ def poll(self, command="start", *, delay=10): self.properties.fast_polling = False _poll_cls = DeviceNormalPoll - if not str(command).lower() in ["stop", "start", "0", "False"]: + if str(command).lower() not in ["stop", "start", "0", "False"]: self._log.error( 'Bad argument for function. Needs "stop", "start", "0" or "False" or provide keyword arg (command or delay)' ) diff --git a/BAC0/core/functions/DeviceCommunicationControl.py b/BAC0/core/functions/DeviceCommunicationControl.py index cae73afd..5afaf2ce 100644 --- a/BAC0/core/functions/DeviceCommunicationControl.py +++ b/BAC0/core/functions/DeviceCommunicationControl.py @@ -78,7 +78,7 @@ def dcc(self, address=None, duration=None, password=None, state=None): if iocb.ioResponse: # successful response apdu = iocb.ioResponse - if not isinstance(iocb.ioResponse, SimpleAckPDU): # expect an ACK + if not isinstance(apdu, SimpleAckPDU): # expect an ACK self._log.warning("Not an ack, see debug for more infos.") self._log.debug( "Not an ack. | APDU : {} / {}".format((apdu, type(apdu))) diff --git a/BAC0/core/functions/Discover.py b/BAC0/core/functions/Discover.py index 8d491911..69966b17 100644 --- a/BAC0/core/functions/Discover.py +++ b/BAC0/core/functions/Discover.py @@ -233,11 +233,7 @@ def _iam_request(self, destination=None): try: # build a response request = IAmRequest() - if destination: - request.pduDestination = destination - else: - request.pduDestination = GlobalBroadcast() - + request.pduDestination = destination if destination else GlobalBroadcast() # fill the response with details about us (from our device object) request.iAmDeviceIdentifier = self.this_device.objectIdentifier request.maxAPDULengthAccepted = self.this_device.maxApduLengthAccepted diff --git a/BAC0/core/functions/Reinitialize.py b/BAC0/core/functions/Reinitialize.py index 52a5bc2c..f96a2b2b 100644 --- a/BAC0/core/functions/Reinitialize.py +++ b/BAC0/core/functions/Reinitialize.py @@ -71,7 +71,7 @@ def reinitialize(self, address=None, password=None, state="coldstart"): if iocb.ioResponse: # successful response apdu = iocb.ioResponse - if not isinstance(iocb.ioResponse, SimpleAckPDU): # expect an ACK + if not isinstance(apdu, SimpleAckPDU): # expect an ACK self._log.warning("Not an ack, see debug for more infos.") self._log.debug( "Not an ack. | APDU : {} / {}".format((apdu, type(apdu))) diff --git a/BAC0/core/functions/TimeSync.py b/BAC0/core/functions/TimeSync.py index 30d52c4b..9c5c68d2 100644 --- a/BAC0/core/functions/TimeSync.py +++ b/BAC0/core/functions/TimeSync.py @@ -46,9 +46,7 @@ def _build_datetime(UTC=False): else: _date = Date().now().value _time = Time().now().value - _datetime = DateTime(date=_date, time=_time) - - return _datetime + return DateTime(date=_date, time=_time) @note_and_log diff --git a/BAC0/core/io/Read.py b/BAC0/core/io/Read.py index be11a78c..cf71d7bc 100644 --- a/BAC0/core/io/Read.py +++ b/BAC0/core/io/Read.py @@ -216,11 +216,8 @@ def _split_the_read_request(self, args, arr_index): number of properties without supporting segmentation (FieldServers are a good example) """ - objlist = [] nmbr_obj = self.read(args, arr_index=0) - for i in range(1, nmbr_obj + 1): - objlist.append(self.read(args, arr_index=i)) - return objlist + 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): """ Build a ReadPropertyMultiple request, wait for the answer and return the values @@ -451,7 +448,7 @@ def build_rpm_request(self, args, vendor_id=0): except: break - elif prop_id in ( + elif prop_id not in ( "all", "required", "optional", @@ -460,8 +457,6 @@ def build_rpm_request(self, args, vendor_id=0): "objectIdentifier", "polarity", ): - pass - else: datatype = get_datatype(obj_type, prop_id, vendor_id=vendor_id) if not datatype: raise ValueError( @@ -640,8 +635,7 @@ def readRange(self, args, arr_index=None, vendor_id=0, bacoid=None, timeout=10): def read_priority_array(self, addr, obj, obj_instance): pa = self.read("{} {} {} priorityArray".format(addr, obj, obj_instance)) - res = [] - res.append(pa) + res = [pa] for each in range(1, 17): _pa = pa[each] for k, v in _pa.__dict__.items(): @@ -679,14 +673,12 @@ def cast_datatype_from_tag(propertyValue, obj_id, prop_id): tag = tag_list[0].tagNumber datatype = Tag._app_tag_class[tag] - value = {"{}_{}".format(obj_id, prop_id): propertyValue.cast_out(datatype)} else: from bacpypes.constructeddata import ArrayOf subtype_tag = propertyValue.tagList.tagList[0].tagList[0].tagNumber datatype = ArrayOf(Tag._app_tag_class[subtype_tag]) - value = {"{}_{}".format(obj_id, prop_id): propertyValue.cast_out(datatype)} - + value = {"{}_{}".format(obj_id, prop_id): propertyValue.cast_out(datatype)} except: value = {"{}_{}".format(obj_id, prop_id): propertyValue} return value diff --git a/BAC0/core/io/Write.py b/BAC0/core/io/Write.py index 00046a4f..56c2eb56 100644 --- a/BAC0/core/io/Write.py +++ b/BAC0/core/io/Write.py @@ -100,7 +100,7 @@ def write(self, args, vendor_id=0, timeout=10): if iocb.ioResponse: # successful response apdu = iocb.ioResponse - if not isinstance(iocb.ioResponse, SimpleAckPDU): # expect an ACK + if not isinstance(apdu, SimpleAckPDU): # expect an ACK self._log.warning("Not an ack, see debug for more infos.") self._log.debug( "Not an ack. | APDU : {} / {}".format((apdu, type(apdu))) @@ -127,9 +127,8 @@ def _parse_wp_args(self, args): obj_inst = int(obj_inst) value = args[3] indx = None - if len(args) >= 5: - if args[4] != "-": - indx = int(args[4]) + if len(args) >= 5 and args[4] != "-": + indx = int(args[4]) priority = None if len(args) >= 6: priority = int(args[5]) @@ -151,16 +150,15 @@ def _validate_value_vs_datatype(self, obj_type, prop_id, indx, vendor_id, value) if value == "null": value = Null() elif issubclass(datatype, Atomic): - if datatype is Integer: + if ( + datatype is Integer + or datatype is not Real + or datatype is Unsigned + or datatype is Enumerated + ): value = int(value) - elif datatype is Real: + else: value = float(value) - elif datatype is Unsigned: - value = int(value) - elif datatype is Enumerated: - value = int(value) - value = datatype(value) - elif issubclass(datatype, Array) and (indx is not None): if indx == 0: value = Integer(value) @@ -272,7 +270,7 @@ def writeMultiple(self, addr=None, args=None, vendor_id=0, timeout=10): if iocb.ioResponse: # successful response apdu = iocb.ioResponse - if not isinstance(iocb.ioResponse, SimpleAckPDU): # expect an ACK + if not isinstance(apdu, SimpleAckPDU): # expect an ACK self._log.warning("Not an ack, see debug for more infos.") self._log.debug( "Not an ack. | APDU : {} / {}".format((apdu, type(apdu))) diff --git a/BAC0/core/proprietary_objects/jci.py b/BAC0/core/proprietary_objects/jci.py index a41aae07..1e773042 100644 --- a/BAC0/core/proprietary_objects/jci.py +++ b/BAC0/core/proprietary_objects/jci.py @@ -120,7 +120,7 @@ def tec_short_point_list(): - lst = [ + return [ ("binaryInput", 30827), ("binaryInput", 30828), ("binaryOutput", 86908), @@ -171,4 +171,3 @@ def tec_short_point_list(): ("analogOutput", 86915), ("multiStateValue", 6), ] - return lst diff --git a/BAC0/core/proprietary_objects/object.py b/BAC0/core/proprietary_objects/object.py index 1115b3e2..1fb9bb80 100644 --- a/BAC0/core/proprietary_objects/object.py +++ b/BAC0/core/proprietary_objects/object.py @@ -2,13 +2,14 @@ # Prochaine étape : créer une focntion qui va lire "all" et se redéfinir dynamiquement def create_proprietary_object(params): - props = [] try: _validate_params(params) except: raise - for k, v in params["properties"].items(): - props.append(Property(v["obj_id"], v["primitive"], mutable=v["mutable"])) + props = [ + Property(v["obj_id"], v["primitive"], mutable=v["mutable"]) + for k, v in params["properties"].items() + ] new_class = type( params["name"], diff --git a/BAC0/infos.py b/BAC0/infos.py index b7b6a05a..528859eb 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.07.08dev" +__version__ = "20.07.10dev" __license__ = "LGPLv3" diff --git a/BAC0/scripts/Complete.py b/BAC0/scripts/Complete.py index 9e2259dd..95569872 100644 --- a/BAC0/scripts/Complete.py +++ b/BAC0/scripts/Complete.py @@ -90,7 +90,7 @@ def number_of_devices_per_network(self): def print_list(self, lst): s = "" try: - s = s + lst[0] + s += lst[0] except IndexError: return s try: diff --git a/BAC0/scripts/Lite.py b/BAC0/scripts/Lite.py index 3c1e561f..723a6870 100755 --- a/BAC0/scripts/Lite.py +++ b/BAC0/scripts/Lite.py @@ -295,8 +295,6 @@ def ping_registered_devices(self): ) ) each.connect(network=self) - else: - pass @property def registered_devices(self): @@ -322,10 +320,7 @@ def add_trend(self, point_to_trend): Argument provided must be of type Point or TrendLog ex. bacnet.add_trend(controller['point_name']) """ - if isinstance(point_to_trend, Point): - oid = id(point_to_trend) - self._points_to_trend[oid] = point_to_trend - elif isinstance(point_to_trend, TrendLog): + if isinstance(point_to_trend, Point) or isinstance(point_to_trend, TrendLog): oid = id(point_to_trend) self._points_to_trend[oid] = point_to_trend else: @@ -338,9 +333,7 @@ def remove_trend(self, point_to_remove): Argument provided must be of type Point or TrendLog ex. bacnet.remove_trend(controller['point_name']) """ - if isinstance(point_to_remove, Point): - oid = id(point_to_remove) - elif isinstance(point_to_remove, TrendLog): + if isinstance(point_to_remove, Point) or isinstance(point_to_remove, TrendLog): oid = id(point_to_remove) else: raise TypeError("Please provide point or trendLog containing history") diff --git a/BAC0/sql/sql.py b/BAC0/sql/sql.py index 72721382..30a377a9 100644 --- a/BAC0/sql/sql.py +++ b/BAC0/sql/sql.py @@ -69,8 +69,7 @@ def points_properties_df(self): p.pop("overridden", None) pprops[each.properties.name] = p - df = pd.DataFrame(pprops) - return df + return pd.DataFrame(pprops) def backup_histories_df(self, resampling="1s"): """ @@ -80,18 +79,25 @@ def backup_histories_df(self, resampling="1s"): if isinstance(resampling, str): resampling_needed = True resampling_freq = resampling - elif resampling == 0 or resampling == False: + elif resampling in [0, False]: resampling_needed = False + print(resampling, resampling_freq, resampling_needed) for point in self.points: try: if point.history.dtypes == object: - backup[point.properties.name] = ( - point.history.replace(["inactive", "active"], [0, 1]) - .resample("1s") - .mean() - ) - except: + if resampling_needed: + backup[point.properties.name] = ( + point.history.replace(["inactive", "active"], [0, 1]) + .resample(resampling_freq) + .last() + ) + else: + backup[point.properties.name] = point.history.replace( + ["inactive", "active"], [0, 1] + ) + except Exception as error: + print(point, error) # probably not enough points... if point.history.dtypes == object: backup[point.properties.name] = point.history.replace( @@ -134,13 +140,13 @@ def save(self, filename=None, resampling="1s"): his.index = his["index"].apply(Timestamp) try: last = his.index[-1] - df_to_backup = self.backup_histories_df()[last:] + df_to_backup = self.backup_histories_df(resampling=resampling)[last:] except IndexError: df_to_backup = self.backup_histories_df(resampling=resampling) else: self._log.debug("Creating a new backup database") - df_to_backup = self.backup_histories_df() + df_to_backup = self.backup_histories_df(resampling=resampling) # DataFrames that will be saved to SQL with contextlib.closing( @@ -162,8 +168,7 @@ def save(self, filename=None, resampling="1s"): ) # Saving other properties to a pickle file... - prop_backup = {} - prop_backup["device"] = self.dev_properties_df() + prop_backup = {"device": self.dev_properties_df()} prop_backup["points"] = self.points_properties_df() with open("{}.bin".format(self.properties.db_name), "wb") as file: pickle.dump(prop_backup, file) diff --git a/BAC0/tasks/Match.py b/BAC0/tasks/Match.py index 85fc7052..96455b69 100644 --- a/BAC0/tasks/Match.py +++ b/BAC0/tasks/Match.py @@ -77,10 +77,7 @@ def __init__(self, value=None, point=None, delay=5, name=None): def task(self): try: - if hasattr(self.value, "__call__"): - value = self.value() - else: - value = self.value + value = self.value() if hasattr(self.value, "__call__") else self.value if value != self.point.value: self.point._set(value) except Exception: diff --git a/BAC0/tasks/TaskManager.py b/BAC0/tasks/TaskManager.py index 8ef57d13..e722e524 100644 --- a/BAC0/tasks/TaskManager.py +++ b/BAC0/tasks/TaskManager.py @@ -29,9 +29,7 @@ def stopAllTasks(): for each in Manager.taskList: each.exitFlag = True while True: - _alive = [] - for each in Manager.taskList: - _alive.append(each.is_alive()) + _alive = [each.is_alive() for each in Manager.taskList] if not any(_alive): clean_tasklist() break @@ -52,7 +50,7 @@ def __init__(self, delay=5, daemon=True, name="recurring"): self.exitFlag = False self.lock = Manager.threadLock self.delay = delay - if not self.name in Manager.taskList: + if self.name not in Manager.taskList: Manager.taskList.append(self) def run(self): @@ -67,7 +65,7 @@ def process(self): # This replace a single time.sleep # the goal is to speed up the stop # of the thread by providing an easy way out - for i in range(self.delay * 2): + for _ in range(self.delay * 2): if self.exitFlag: break time.sleep(0.5) @@ -94,7 +92,7 @@ class OneShotTask(Thread): def __init__(self, daemon=True, name="Oneshot"): Thread.__init__(self, name=name, daemon=daemon) self.lock = Manager.threadLock - if not self.name in Manager.taskList: + if self.name not in Manager.taskList: Manager.taskList.append(self) def run(self): From c499684a94ac35dd8d51976be8fda1954443670a Mon Sep 17 00:00:00 2001 From: Joshua Runyan Date: Fri, 24 Jul 2020 16:23:34 -0400 Subject: [PATCH 18/27] Added description as a configurable item --- BAC0/scripts/Base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/BAC0/scripts/Base.py b/BAC0/scripts/Base.py index 7a1cdd34..71571aab 100644 --- a/BAC0/scripts/Base.py +++ b/BAC0/scripts/Base.py @@ -85,6 +85,7 @@ def __init__( modelName=CharacterString("BAC0 Scripting Tool"), vendorId=842, vendorName=CharacterString("SERVISYS inc."), + description=CharacterString("http://christiantremblay.github.io/BAC0/") ): self._log.debug("Configurating app") @@ -130,6 +131,7 @@ def __init__( self.vendorId = vendorId self.vendorName = vendorName self.modelName = modelName + self.description = description self.discoveredDevices = None self.systemStatus = DeviceStatus(1) @@ -161,7 +163,7 @@ def startApp(self): vendorName=self.vendorName, modelName=self.modelName, systemStatus=self.systemStatus, - description="http://christiantremblay.github.io/BAC0/", + description=self.description, firmwareRevision=self.firmwareRevision, applicationSoftwareVersion=infos.__version__, protocolVersion=1, From d491703b08af8177701adf4e9a6aca3e48204ef1 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Thu, 30 Jul 2020 16:43:21 -0400 Subject: [PATCH 19/27] Addind WhoHas IHave feature with test --- BAC0/core/functions/Discover.py | 66 ++++++++++++++++++++++++++++++++- tests/test_WhoHasIHave.py | 16 ++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 tests/test_WhoHasIHave.py diff --git a/BAC0/core/functions/Discover.py b/BAC0/core/functions/Discover.py index 69966b17..eb51680d 100644 --- a/BAC0/core/functions/Discover.py +++ b/BAC0/core/functions/Discover.py @@ -14,10 +14,16 @@ import time # --- 3rd party modules --- -from bacpypes.apdu import WhoIsRequest, IAmRequest +from bacpypes.apdu import ( + WhoIsRequest, + IAmRequest, + WhoHasRequest, + WhoHasLimits, + WhoHasObject, +) from bacpypes.core import deferred from bacpypes.pdu import Address, GlobalBroadcast, LocalBroadcast -from bacpypes.primitivedata import Unsigned +from bacpypes.primitivedata import Unsigned, ObjectIdentifier, CharacterString from bacpypes.constructeddata import Array from bacpypes.object import get_object_class, get_datatype from bacpypes.iocb import IOCB, SieveQueue, IOController @@ -341,6 +347,62 @@ def what_is_network_number(self, destination=None): deferred(self.this_application.nse.request_io, iocb) iocb.wait() + def whohas( + self, + object_id=None, + object_name=None, + instance_range_low_limit=0, + instance_range_high_limit=4194303, + destination=None, + global_broadcast=False, + ): + """ + Object ID : analogInput:1 + Object Name : string + Instance Range Low Limit : 0 + Instance Range High Limit : 4194303 + destination (optional) : If empty, local broadcast will be used. + global_broadcast : False + + """ + obj_id = ObjectIdentifier(object_id) + if object_name and not object_id: + obj_name = CharacterString(object_name) + obj = WhoHasObject(objectName=obj_name) + elif object_id and not object_name: + obj = WhoHasObject(objectIdentifier=obj_id) + else: + obj = WhoHasObject(objectIdentifier=obj_id, objectName=obj_name) + limits = WhoHasLimits( + deviceInstanceRangeLowLimit=instance_range_low_limit, + deviceInstanceRangeHighLimit=instance_range_high_limit, + ) + request = WhoHasRequest(object=obj, limits=limits) + if destination: + request.pduDestination = Address(destination) + else: + if global_broadcast: + request.pduDestination = GlobalBroadcast() + else: + request.pduDestination = LocalBroadcast() + iocb = IOCB(request) # make an IOCB + iocb.set_timeout(2) + deferred(self.this_application.request_io, iocb) + iocb.wait() + + iocb = IOCB(request) # make an IOCB + self.this_application._last_i_have_received = [] + + if iocb.ioResponse: # successful response + apdu = iocb.ioResponse + + if iocb.ioError: # unsuccessful: error/reject/abort + pass + + time.sleep(3) + # self.discoveredObjects = self.this_application.i_am_counter + return self.this_application._last_i_have_received + rejectMessageToNetworkReasons = [ "Other Error", diff --git a/tests/test_WhoHasIHave.py b/tests/test_WhoHasIHave.py new file mode 100644 index 00000000..0f3d8799 --- /dev/null +++ b/tests/test_WhoHasIHave.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python + +import pytest + +def test_WhoHas(network_and_devices): + # Write to an object and validate new value is correct + bacnet = network_and_devices.bacnet + test_device = network_and_devices.test_device + response = bacnet.whohas("analogInput:0", destination='{}'.format(test_device.properties.address)) + assert response + #response = bacnet.whohas("analogInput:0", global_broadcast=True) + #assert response + #response = bacnet.whohas("analogInput:0") + #assert response + # Can't work as I'm using different ports to have multiple devices using the same IP.... + # So neither local or global broadcast will give result here From 4d6a7aaca10fe99c9f39746e2c8c4913d416a138 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Thu, 30 Jul 2020 16:49:52 -0400 Subject: [PATCH 20/27] Addind WhoHas IHave feature with test. New version of test to define objects in a better way. Corrections to write function broken by sourcery...and me :) --- BAC0/core/app/ScriptApplication.py | 26 +++++- BAC0/core/devices/Device.py | 3 +- BAC0/core/devices/Points.py | 80 +++++++++++------- BAC0/core/devices/create_objects.py | 7 ++ BAC0/core/io/Write.py | 14 ++- BAC0/infos.py | 2 +- BAC0/scripts/Base.py | 2 +- tests/conftest.py | 127 +++++++++++++++++++++++++--- tests/test_WhoHasIHave.py | 13 +-- tests/test_Write.py | 6 ++ 10 files changed, 229 insertions(+), 51 deletions(-) diff --git a/BAC0/core/app/ScriptApplication.py b/BAC0/core/app/ScriptApplication.py index c402b30a..dfb9ed7f 100644 --- a/BAC0/core/app/ScriptApplication.py +++ b/BAC0/core/app/ScriptApplication.py @@ -31,7 +31,7 @@ from bacpypes.core import deferred # basic services -from bacpypes.service.device import WhoIsIAmServices +from bacpypes.service.device import WhoIsIAmServices, WhoHasIHaveServices from bacpypes.service.object import ReadWritePropertyServices # --- this application's modules --- @@ -45,6 +45,7 @@ class BAC0Application( ApplicationIOController, WhoIsIAmServices, + WhoHasIHaveServices, ReadWritePropertyServices, ReadWritePropertyMultipleServices, ): @@ -112,11 +113,13 @@ def __init__( self.nsap.bind(self.bip, address=self.localAddress) self.i_am_counter = defaultdict(int) + self.i_have_counter = defaultdict(int) self.who_is_counter = defaultdict(int) # keep track of requests to line up responses self._request = None self._last_i_am_received = [] + self._last_i_have_received = [] def do_IAmRequest(self, apdu): """Given an I-Am request, cache it.""" @@ -127,6 +130,15 @@ def do_IAmRequest(self, apdu): self.i_am_counter[key] += 1 self._last_i_am_received.append(key) + def do_IHaveRequest(self, apdu): + """Given an I-Have request, cache it.""" + self._log.debug("do_IHaveRequest {!r}".format(apdu)) + + # build a key from the source, using object name + key = (str(apdu.pduSource), apdu.objectName) + self.i_have_counter[key] += 1 + self._last_i_have_received.append(key) + def do_WhoIsRequest(self, apdu): """Respond to a Who-Is request.""" @@ -174,6 +186,7 @@ def request(self, apdu): class BAC0ForeignDeviceApplication( ApplicationIOController, WhoIsIAmServices, + WhoHasIHaveServices, ReadWritePropertyServices, ReadWritePropertyMultipleServices, ): @@ -241,10 +254,12 @@ def __init__( self.nsap.bind(self.bip) self.i_am_counter = defaultdict(int) + self.i_have_counter = defaultdict(int) self.who_is_counter = defaultdict(int) # keep track of requests to line up responses self._request = None self._last_i_am_received = [] + self._last_i_have_received = [] def do_IAmRequest(self, apdu): """Given an I-Am request, cache it.""" @@ -256,6 +271,15 @@ def do_IAmRequest(self, apdu): self._last_i_am_received.append(key) # continue with the default implementation + def do_IHaveRequest(self, apdu): + """Given an I-Have request, cache it.""" + self._log.debug("do_IHaveRequest {!r}".format(apdu)) + + # build a key from the source, using object name + key = (str(apdu.pduSource), apdu.objectName) + self.i_have_counter[key] += 1 + self._last_i_have_received.append(key) + def do_WhoIsRequest(self, apdu): """Respond to a Who-Is request.""" self._log.debug("do_WhoIsRequest {!r}".format(apdu)) diff --git a/BAC0/core/devices/Device.py b/BAC0/core/devices/Device.py index 30399d5e..1163c3a1 100755 --- a/BAC0/core/devices/Device.py +++ b/BAC0/core/devices/Device.py @@ -43,6 +43,7 @@ SegmentationNotSupported, BadDeviceDefinition, RemovedPointException, + WritePropertyException, ) # from ...bokeh.BokehRenderer import BokehPlot @@ -588,7 +589,7 @@ def __setitem__(self, point_name, value): """ try: self._findPoint(point_name)._set(value) - except ValueError as ve: + except WritePropertyException as ve: self._log.error("{}".format(ve)) def __len__(self): diff --git a/BAC0/core/devices/Points.py b/BAC0/core/devices/Points.py index 8c86575f..687d58a2 100644 --- a/BAC0/core/devices/Points.py +++ b/BAC0/core/devices/Points.py @@ -13,7 +13,15 @@ from collections import namedtuple import time -from bacpypes.primitivedata import CharacterString +from bacpypes.primitivedata import ( + CharacterString, + Null, + Atomic, + Integer, + Unsigned, + Real, + Enumerated, +) # --- 3rd party modules --- try: @@ -35,6 +43,7 @@ NoResponseFromController, UnknownPropertyError, RemovedPointException, + WritePropertyException, ) from ..utils.notes import note_and_log @@ -464,7 +473,7 @@ def _set(self, value): Allows the syntax: device['point'] = value """ - raise Exception("Must be overridden") + raise NotImplementedError("Must be overridden") def poll(self, command="start", *, delay=10): """ @@ -608,8 +617,10 @@ def _set(self, value): val = float(value) if isinstance(val, float): self._setitem(value) - except: - raise ValueError("Value must be numeric") + except Exception as error: + raise WritePropertyException( + "Problem writing to device : {}".format(error) + ) def __repr__(self): polling = self.properties.device.properties.pollDelay @@ -732,14 +743,19 @@ def units(self): return None def _set(self, value): - if value == True: - self._setitem("active") - elif value == False: - self._setitem("inactive") - elif str(value) in ["inactive", "active"] or str(value).lower() == "auto": - self._setitem(value) - else: - raise ValueError('Value must be boolean True, False or "active"/"inactive"') + try: + if value == True: + self._setitem("active") + elif value == False: + self._setitem("inactive") + elif str(value) in ["inactive", "active"] or str(value).lower() == "auto": + self._setitem(value) + else: + raise ValueError( + 'Value must be boolean True, False or "active"/"inactive"' + ) + except (Exception, ValueError) as error: + raise WritePropertyException("Problem writing to device : {}".format(error)) def __repr__(self): return "{}/{} : {}".format( @@ -816,18 +832,21 @@ def units(self): return None def _set(self, value): - if isinstance(value, int): - self._setitem(value) - elif str(value) in self.properties.units_state: - self._setitem(self.properties.units_state.index(value) + 1) - elif str(value).lower() == "auto": - self._setitem("auto") - else: - raise ValueError( - "Value must be integer or correct enum state : {}".format( - self.properties.units_state + try: + if isinstance(value, int): + self._setitem(value) + elif str(value) in self.properties.units_state: + self._setitem(self.properties.units_state.index(value) + 1) + elif str(value).lower() == "auto": + self._setitem("auto") + else: + raise ValueError( + "Value must be integer or correct enum state : {}".format( + self.properties.units_state + ) ) - ) + except (Exception, ValueError) as error: + raise WritePropertyException("Problem writing to device : {}".format(error)) def __repr__(self): return "{}/{} : {}".format( @@ -874,12 +893,15 @@ def units(self): return None def _set(self, value): - if isinstance(value, str): - self._setitem(value) - elif isinstance(value, CharacterString): - self._setitem(value.value) - else: - raise ValueError("Value must be string or CharacterString") + try: + if isinstance(value, str): + self._setitem(value) + elif isinstance(value, CharacterString): + self._setitem(value.value) + else: + raise ValueError("Value must be string or CharacterString") + except (Exception, ValueError) as error: + raise WritePropertyException("Problem writing to device : {}".format(error)) def __repr__(self): return "{}/{} : {}".format( diff --git a/BAC0/core/devices/create_objects.py b/BAC0/core/devices/create_objects.py index dd607b53..7734251f 100644 --- a/BAC0/core/devices/create_objects.py +++ b/BAC0/core/devices/create_objects.py @@ -12,6 +12,13 @@ register_object_type, ) +from bacpypes.local.object import ( + AnalogOutputCmdObject, + AnalogValueCmdObject, + BinaryOutputCmdObject, + BinaryValueCmdObject, +) + from bacpypes.primitivedata import CharacterString, Date, Time, Real, Boolean from bacpypes.constructeddata import ArrayOf from bacpypes.basetypes import EngineeringUnits, DateTime, PriorityArray diff --git a/BAC0/core/io/Write.py b/BAC0/core/io/Write.py index 56c2eb56..37387b83 100644 --- a/BAC0/core/io/Write.py +++ b/BAC0/core/io/Write.py @@ -144,7 +144,10 @@ def _validate_value_vs_datatype(self, obj_type, prop_id, indx, vendor_id, value) This will ensure the value can be encoded and is valid in the context """ # get the datatype + print(obj_type, prop_id, vendor_id) datatype = get_datatype(obj_type, prop_id, vendor_id=vendor_id) + print(datatype) + print(type(datatype)) # change atomic values into something encodeable, null is a special # case if value == "null": @@ -152,13 +155,19 @@ def _validate_value_vs_datatype(self, obj_type, prop_id, indx, vendor_id, value) elif issubclass(datatype, Atomic): if ( datatype is Integer - or datatype is not Real + # or datatype is not Real or datatype is Unsigned or datatype is Enumerated ): value = int(value) - else: + elif datatype is Real: value = float(value) + # value = datatype(value) + else: + # value = float(value) + value = datatype(value) + + value = datatype(value) elif issubclass(datatype, Array) and (indx is not None): if indx == 0: value = Integer(value) @@ -175,6 +184,7 @@ def _validate_value_vs_datatype(self, obj_type, prop_id, indx, vendor_id, value) raise TypeError( "invalid result datatype, expecting {}".format((datatype.__name__,)) ) + self._log.debug("{:<20} {!r} {}".format("Encodeable value", value, type(value))) _value = Any() diff --git a/BAC0/infos.py b/BAC0/infos.py index 528859eb..e0ba3e7a 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.07.10dev" +__version__ = "20.07.28dev" __license__ = "LGPLv3" diff --git a/BAC0/scripts/Base.py b/BAC0/scripts/Base.py index 71571aab..4e0a7b5a 100644 --- a/BAC0/scripts/Base.py +++ b/BAC0/scripts/Base.py @@ -85,7 +85,7 @@ def __init__( modelName=CharacterString("BAC0 Scripting Tool"), vendorId=842, vendorName=CharacterString("SERVISYS inc."), - description=CharacterString("http://christiantremblay.github.io/BAC0/") + description=CharacterString("http://christiantremblay.github.io/BAC0/"), ): self._log.debug("Configurating app") diff --git a/tests/conftest.py b/tests/conftest.py index 0962baf0..c042f001 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,17 +19,49 @@ create_CharStrValue, ) + from collections import namedtuple import time -from bacpypes.primitivedata import CharacterString -from bacpypes.basetypes import EngineeringUnits +from bacpypes.primitivedata import CharacterString, Date, Time, Real, Boolean, Integer +from bacpypes.basetypes import EngineeringUnits, BinaryPV + +from bacpypes.local.object import ( + AnalogOutputCmdObject, + AnalogValueCmdObject, + BinaryOutputCmdObject, + BinaryValueCmdObject, + MultiStateValueCmdObject, + CharacterStringValueCmdObject, +) + +from bacpypes.object import ( + MultiStateValueObject, + AnalogValueObject, + BinaryValueObject, + AnalogInputObject, + BinaryInputObject, + AnalogOutputObject, + BinaryOutputObject, + CharacterStringValueObject, + DateTimeValueObject, + Property, + register_object_type, +) +from bacpypes.constructeddata import ArrayOf @pytest.fixture(scope="session") def network_and_devices(): bacnet = BAC0.lite() + # Register class to activate behaviours + register_object_type(AnalogOutputCmdObject, vendor_id=842) + register_object_type(AnalogValueCmdObject, vendor_id=842) + register_object_type(BinaryOutputCmdObject, vendor_id=842) + register_object_type(BinaryValueCmdObject, vendor_id=842) + register_object_type(MultiStateValueCmdObject, vendor_id=842) + def _add_points(qty, device): # Add a lot of points for tests (segmentation required) mvs = [] @@ -42,16 +74,89 @@ def _add_points(qty, device): charstr = [] for i in range(qty): - mvs.append(create_MV(oid=i, name="mv{}".format(i), pv=1, pv_writable=True)) - new_av = create_AV(oid=i, name="av{}".format(i), pv=99.9, pv_writable=True) - new_av.units = EngineeringUnits.enumerations["degreesCelsius"] - new_av.description = "Fake Description {}".format(i) + states = ["red", "green", "blue"] + new_mv = MultiStateValueCmdObject( + objectIdentifier=("multiStateValue", i), + objectName="mv{}".format(i), + presentValue=1, + numberOfStates=len(states), + stateText=ArrayOf(CharacterString)(states), + description=CharacterString( + "MultiState Value Description {}".format(i) + ), + ) + new_mv.add_property( + Property("relinquishDefault", Integer, default=0, mutable=True) + ) + mvs.append(new_mv) + + new_av = AnalogValueCmdObject( + objectIdentifier=("analogValue", i), + objectName="av{}".format(i), + presentValue=99.9, + description=CharacterString("AnalogValue Description {}".format(i)), + units=EngineeringUnits.enumerations["degreesCelsius"], + ) + new_av.add_property( + Property("relinquishDefault", Real, default=0, mutable=True) + ) avs.append(new_av) - bvs.append(create_BV(oid=i, name="bv{}".format(i), pv=1, pv_writable=True)) - ais.append(create_AI(oid=i, name="ai{}".format(i), pv=99.9)) - aos.append(create_AO(oid=i, name="ao{}".format(i), pv=99.9)) - bis.append(create_BI(oid=i, name="bi{}".format(i), pv=1)) - bos.append(create_BO(oid=i, name="bo{}".format(i), pv=1)) + + new_bv = BinaryValueCmdObject( + objectIdentifier=("binaryValue", i), + objectName="bv{}".format(i), + presentValue="active", + description=CharacterString("Binary Value Description {}".format(i)), + ) + new_bv.add_property( + Property( + "relinquishDefault", BinaryPV, default="inactive", mutable=True + ) + ) + bvs.append(new_bv) + + new_ai = AnalogInputObject( + objectIdentifier=i, + objectName="ai{}".format(i), + presentValue=99.9, + units=EngineeringUnits.enumerations["percent"], + description=CharacterString("AnalogInput Description {}".format(i)), + ) + new_ai.add_property( + Property("outOfService", Boolean, default=False, mutable=True) + ) + ais.append(new_ai) + + new_ao = AnalogOutputCmdObject( + objectIdentifier=("analogOutput", i), + objectName="ao{}".format(i), + presentValue=99.9, + units=EngineeringUnits.enumerations["percent"], + description=CharacterString("AnalogOutput Description {}".format(i)), + ) + aos.append(new_ao) + + new_bi = BinaryInputObject( + objectIdentifier=i, + objectName="bi{}".format(i), + presentValue="active", + description=CharacterString("BinaryInput Description {}".format(i)), + ) + new_bi.add_property( + Property("outOfService", Boolean, default=False, mutable=True) + ) + bis.append(new_bi) + + bos.append( + BinaryOutputCmdObject( + objectIdentifier=("binaryOutput", i), + objectName="bo{}".format(i), + presentValue="active", + description=CharacterString( + "BinaryOutput Description {}".format(i) + ), + ) + ) charstr.append( create_CharStrValue( oid=i, diff --git a/tests/test_WhoHasIHave.py b/tests/test_WhoHasIHave.py index 0f3d8799..0ff7e86b 100644 --- a/tests/test_WhoHasIHave.py +++ b/tests/test_WhoHasIHave.py @@ -2,15 +2,18 @@ import pytest + def test_WhoHas(network_and_devices): # Write to an object and validate new value is correct bacnet = network_and_devices.bacnet test_device = network_and_devices.test_device - response = bacnet.whohas("analogInput:0", destination='{}'.format(test_device.properties.address)) + response = bacnet.whohas( + "analogInput:0", destination="{}".format(test_device.properties.address) + ) assert response - #response = bacnet.whohas("analogInput:0", global_broadcast=True) - #assert response - #response = bacnet.whohas("analogInput:0") - #assert response + # response = bacnet.whohas("analogInput:0", global_broadcast=True) + # assert response + # response = bacnet.whohas("analogInput:0") + # assert response # Can't work as I'm using different ports to have multiple devices using the same IP.... # So neither local or global broadcast will give result here diff --git a/tests/test_Write.py b/tests/test_Write.py index 6b4f8095..9407a643 100644 --- a/tests/test_Write.py +++ b/tests/test_Write.py @@ -6,6 +6,7 @@ """ from bacpypes.primitivedata import CharacterString +import time NEWCSVALUE = CharacterString("New_Test") @@ -15,6 +16,7 @@ def test_WriteAV(network_and_devices): test_device = network_and_devices.test_device old_value = test_device["av0"].value test_device["av0"] = 11.2 + # time.sleep(1) new_value = test_device["av0"].value assert (new_value - 11.2) < 0.01 @@ -24,6 +26,7 @@ def test_RelinquishDefault(network_and_devices): test_device = network_and_devices.test_device old_value = test_device["av0"].value test_device["av0"].default(90) + # time.sleep(1) new_value = test_device["av0"].value assert (new_value - 90) < 0.01 @@ -32,6 +35,7 @@ def test_WriteCharStr(network_and_devices): # Write to an object and validate new value is correct test_device = network_and_devices.test_device test_device["string0"] = NEWCSVALUE.value + # time.sleep(1) new_value = test_device["string0"].value assert new_value == NEWCSVALUE.value @@ -40,6 +44,7 @@ def test_SimulateAI(network_and_devices): # Write to an object and validate new value is correct test_device = network_and_devices.test_device test_device["ai0"] = 1 + # time.sleep(1) new_value = test_device["ai0"].value assert test_device.read_property(("analogInput", 0, "outOfService")) # something is missing so pv can be written to if outOfService == True @@ -50,6 +55,7 @@ def test_RevertSimulation(network_and_devices): # Write to an object and validate new value is correct test_device = network_and_devices.test_device test_device["ai0"] = "auto" + # time.sleep(1) new_value = test_device["ai0"].value assert not test_device.read_property(("analogInput", 0, "outOfService")) assert (new_value - 99.9) < 0.01 From 7bf7fd1380f95823735e15176e134e29ddc7f1c4 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Mon, 3 Aug 2020 14:53:13 -0400 Subject: [PATCH 21/27] Added BBMD feature to BAC0. Providing a bdtable list is sufficient for the app to register itself as a BBMD device on the subnet. ex. bacnet = BAC0.lite(bdtable=['192.168.211.54/24:47808','192.168.210.253/32:47808']) --- BAC0/core/app/ScriptApplication.py | 256 +++++++++++++++++++---------- BAC0/infos.py | 2 +- BAC0/scripts/Base.py | 18 +- BAC0/scripts/Lite.py | 2 + 4 files changed, 188 insertions(+), 90 deletions(-) diff --git a/BAC0/core/app/ScriptApplication.py b/BAC0/core/app/ScriptApplication.py index dfb9ed7f..83a19743 100644 --- a/BAC0/core/app/ScriptApplication.py +++ b/BAC0/core/app/ScriptApplication.py @@ -24,9 +24,15 @@ from bacpypes.pdu import Address from bacpypes.service.object import ReadWritePropertyMultipleServices from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement -from bacpypes.bvllservice import BIPSimple, BIPForeign, AnnexJCodec, UDPMultiplexer +from bacpypes.bvllservice import ( + BIPSimple, + BIPForeign, + BIPBBMD, + AnnexJCodec, + UDPMultiplexer, +) from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint -from bacpypes.comm import ApplicationServiceElement, bind +from bacpypes.comm import ApplicationServiceElement, bind, Client from bacpypes.iocb import IOCB from bacpypes.core import deferred @@ -41,8 +47,60 @@ # ------------------------------------------------------------------------------ +class common_mixin: + def do_IAmRequest(self, apdu): + """Given an I-Am request, cache it.""" + self._log.debug("do_IAmRequest {!r}".format(apdu)) + + # build a key from the source, just use the instance number + key = (str(apdu.pduSource), apdu.iAmDeviceIdentifier[1]) + self.i_am_counter[key] += 1 + self._last_i_am_received.append(key) + + def do_IHaveRequest(self, apdu): + """Given an I-Have request, cache it.""" + self._log.debug("do_IHaveRequest {!r}".format(apdu)) + + # build a key from the source, using object name + key = (str(apdu.pduSource), apdu.objectName) + self.i_have_counter[key] += 1 + self._last_i_have_received.append(key) + + def do_WhoIsRequest(self, apdu): + """Respond to a Who-Is request.""" + + # build a key from the source and parameters + key = ( + str(apdu.pduSource), + apdu.deviceInstanceRangeLowLimit, + apdu.deviceInstanceRangeHighLimit, + ) + self._log.debug( + "do_WhoIsRequest from {} | {} to {}".format(key[0], key[1], key[2]) + ) + + # count the times this has been received + self.who_is_counter[key] += 1 + low_limit = key[1] + high_limit = key[2] + + # count the times this has been received + self.who_is_counter[key] += 1 + + if low_limit is not None and self.localDevice.objectIdentifier[1] < low_limit: + return + if high_limit is not None and self.localDevice.objectIdentifier[1] > high_limit: + return + # generate an I-Am + self._log.debug("Responding to Who is by a Iam") + self.iam_req.pduDestination = apdu.pduSource + iocb = IOCB(self.iam_req) # make an IOCB + deferred(self.request_io, iocb) + + @note_and_log class BAC0Application( + common_mixin, ApplicationIOController, WhoIsIAmServices, WhoHasIHaveServices, @@ -121,55 +179,6 @@ def __init__( self._last_i_am_received = [] self._last_i_have_received = [] - def do_IAmRequest(self, apdu): - """Given an I-Am request, cache it.""" - self._log.debug("do_IAmRequest {!r}".format(apdu)) - - # build a key from the source, just use the instance number - key = (str(apdu.pduSource), apdu.iAmDeviceIdentifier[1]) - self.i_am_counter[key] += 1 - self._last_i_am_received.append(key) - - def do_IHaveRequest(self, apdu): - """Given an I-Have request, cache it.""" - self._log.debug("do_IHaveRequest {!r}".format(apdu)) - - # build a key from the source, using object name - key = (str(apdu.pduSource), apdu.objectName) - self.i_have_counter[key] += 1 - self._last_i_have_received.append(key) - - def do_WhoIsRequest(self, apdu): - """Respond to a Who-Is request.""" - - # build a key from the source and parameters - key = ( - str(apdu.pduSource), - apdu.deviceInstanceRangeLowLimit, - apdu.deviceInstanceRangeHighLimit, - ) - self._log.debug( - "do_WhoIsRequest from {} | {} to {}".format(key[0], key[1], key[2]) - ) - - # count the times this has been received - self.who_is_counter[key] += 1 - low_limit = key[1] - high_limit = key[2] - - # count the times this has been received - self.who_is_counter[key] += 1 - - if low_limit is not None and self.localDevice.objectIdentifier[1] < low_limit: - return - if high_limit is not None and self.localDevice.objectIdentifier[1] > high_limit: - return - # generate an I-Am - self._log.debug("Responding to Who is by a Iam") - self.iam_req.pduDestination = apdu.pduSource - iocb = IOCB(self.iam_req) # make an IOCB - deferred(self.request_io, iocb) - def close_socket(self): # pass to the multiplexer, then down to the sockets self.mux.close_socket() @@ -184,6 +193,7 @@ def request(self, apdu): @note_and_log class BAC0ForeignDeviceApplication( + common_mixin, ApplicationIOController, WhoIsIAmServices, WhoHasIHaveServices, @@ -261,50 +271,122 @@ def __init__( self._last_i_am_received = [] self._last_i_have_received = [] - def do_IAmRequest(self, apdu): - """Given an I-Am request, cache it.""" - self._log.debug("do_IAmRequest {!r}".format(apdu)) + def close_socket(self): + # pass to the multiplexer, then down to the sockets + self.mux.close_socket() - # build a key from the source, just use the instance number - key = (str(apdu.pduSource), apdu.iAmDeviceIdentifier[1]) - self.i_am_counter[key] += 1 - self._last_i_am_received.append(key) - # continue with the default implementation - def do_IHaveRequest(self, apdu): - """Given an I-Have request, cache it.""" - self._log.debug("do_IHaveRequest {!r}".format(apdu)) +class NullClient(Client): + def __init__(self, cid=None): + Client.__init__(self, cid=cid) - # build a key from the source, using object name - key = (str(apdu.pduSource), apdu.objectName) - self.i_have_counter[key] += 1 - self._last_i_have_received.append(key) + def confirmation(self, *args, **kwargs): + pass - def do_WhoIsRequest(self, apdu): - """Respond to a Who-Is request.""" - self._log.debug("do_WhoIsRequest {!r}".format(apdu)) - # build a key from the source and parameters - key = ( - str(apdu.pduSource), - apdu.deviceInstanceRangeLowLimit, - apdu.deviceInstanceRangeHighLimit, +@note_and_log +class BAC0BBMDDeviceApplication( + common_mixin, + ApplicationIOController, + WhoIsIAmServices, + WhoHasIHaveServices, + ReadWritePropertyServices, + ReadWritePropertyMultipleServices, +): + """ + Defines a basic BACnet/IP application to process BACnet requests. + + :param *args: local object device, local IP address + See BAC0.scripts.BasicScript for more details. + + """ + + bdt = [] + + def __init__( + self, + localDevice, + localAddress, + bdtable=[], + deviceInfoCache=None, + aseID=None, + iam_req=None, + ): + + self.bdtable = bdtable + + null_client = NullClient() + + ApplicationIOController.__init__( + self, localDevice, deviceInfoCache, aseID=aseID ) - low_limit = key[1] - high_limit = key[2] - # count the times this has been received - self.who_is_counter[key] += 1 + self.iam_req = iam_req + # local address might be useful for subclasses + if isinstance(localAddress, Address): + self.localAddress = localAddress + else: + self.localAddress = Address(localAddress) - if low_limit is not None and self.localDevice.objectIdentifier[1] < low_limit: - return - if high_limit is not None and self.localDevice.objectIdentifier[1] > high_limit: - return - # generate an I-Am - self._log.debug("Responding to Who is by a Iam") - self.iam_req.pduDestination = apdu.pduSource - iocb = IOCB(self.iam_req) # make an IOCB - deferred(self.request_io, iocb) + # 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 = NetworkServiceElementWithRequests() + 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 = BIPBBMD(self.localAddress) + self.annexj = AnnexJCodec() + self.mux = UDPMultiplexer(self.localAddress, noBroadcast=True) + + # bind the bottom layers + # bind(self.bip, self.annexj, self.mux.annexJ) + bind(null_client, self.bip, self.annexj, self.mux.annexJ) + + if self.bdtable: + for bdtentry in self.bdtable: + self.add_peer(bdtentry) + + # bind the NSAP to the stack, no network number + self.nsap.bind(self.bip) + + self.i_am_counter = defaultdict(int) + self.i_have_counter = defaultdict(int) + self.who_is_counter = defaultdict(int) + # keep track of requests to line up responses + self._request = None + self._last_i_am_received = [] + self._last_i_have_received = [] + + def add_peer(self, address): + try: + bdt_address = Address(address) + self.bip.add_peer(bdt_address) + except Exception: + raise + + def remove_peer(self, address): + try: + bdt_address = Address(address) + self.bip.remove_peer(bdt_address) + except Exception: + raise def close_socket(self): # pass to the multiplexer, then down to the sockets diff --git a/BAC0/infos.py b/BAC0/infos.py index e0ba3e7a..34ba9c1f 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.07.28dev" +__version__ = "20.08.03dev" __license__ = "LGPLv3" diff --git a/BAC0/scripts/Base.py b/BAC0/scripts/Base.py index 4e0a7b5a..403a9b05 100644 --- a/BAC0/scripts/Base.py +++ b/BAC0/scripts/Base.py @@ -34,7 +34,11 @@ def stopApp() from bacpypes.primitivedata import CharacterString # --- this application's modules --- -from ..core.app.ScriptApplication import BAC0Application, BAC0ForeignDeviceApplication +from ..core.app.ScriptApplication import ( + BAC0Application, + BAC0ForeignDeviceApplication, + BAC0BBMDDeviceApplication, +) from .. import infos from ..core.io.IOExceptions import InitializationError from ..core.functions.GetIPAddr import validate_ip_address @@ -82,6 +86,7 @@ def __init__( segmentationSupported="segmentedBoth", bbmdAddress=None, bbmdTTL=0, + bdtable=None, modelName=CharacterString("BAC0 Scripting Tool"), vendorId=842, vendorName=CharacterString("SERVISYS inc."), @@ -138,6 +143,7 @@ def __init__( self.bbmdAddress = bbmdAddress self.bbmdTTL = bbmdTTL + self.bdtable = bdtable self.firmwareRevision = firmwareRevision @@ -171,7 +177,15 @@ def startApp(self): ) # make an application - if self.bbmdAddress and self.bbmdTTL > 0: + if self.bdtable: + self.this_application = BAC0BBMDDeviceApplication( + self.this_device, + self.localIPAddr, + bdtable=self.bdtable, + iam_req=self._iam_request(), + ) + app_type = "BBMD Device" + elif self.bbmdAddress and self.bbmdTTL > 0: self.this_application = BAC0ForeignDeviceApplication( self.this_device, diff --git a/BAC0/scripts/Lite.py b/BAC0/scripts/Lite.py index 723a6870..eaf0b54b 100755 --- a/BAC0/scripts/Lite.py +++ b/BAC0/scripts/Lite.py @@ -88,6 +88,7 @@ def __init__( mask=None, bbmdAddress=None, bbmdTTL=0, + bdtable=None, ping=True, **params ): @@ -134,6 +135,7 @@ def __init__( localIPAddr=ip_addr, bbmdAddress=bbmdAddress, bbmdTTL=bbmdTTL, + bdtable=bdtable, **params ) From 7aeddc49bdb60abe706c323aaa46f7a711c23359 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Fri, 21 Aug 2020 09:44:07 -0400 Subject: [PATCH 22/27] Switched to netifaces to find netmask. It simplify the code and will cover cases where ifconfig is not available --- BAC0/core/functions/GetIPAddr.py | 51 +++++++------------------------- requirements.txt | 1 + 2 files changed, 12 insertions(+), 40 deletions(-) diff --git a/BAC0/core/functions/GetIPAddr.py b/BAC0/core/functions/GetIPAddr.py index 7de861a0..0190ff5f 100644 --- a/BAC0/core/functions/GetIPAddr.py +++ b/BAC0/core/functions/GetIPAddr.py @@ -20,6 +20,7 @@ import ipaddress import sys import re +import netifaces class HostIP: @@ -103,49 +104,19 @@ def _findSubnetMask(self, ip): a default IP address when defining Script :param ip: (str) optionnal IP address. If not provided, default to getIPAddr() - :param mask: (str) optionnal subnet mask. If not provided, will try to find one using ipconfig (Windows) or ifconfig (Linux or MAC) - + :returns: broadcast IP Adress as String """ - ip = ip - - if "win32" in sys.platform: - try: - proc = subprocess.Popen("ipconfig", stdout=subprocess.PIPE) - while True: - line = proc.stdout.readline() - if ip.encode() in line: - break - mask = ( - proc.stdout.readline() - .rstrip() - .split(b":")[-1] - .replace(b" ", b"") - .decode() - ) - except: - raise NetworkInterfaceException("Cannot read IP parameters from OS") - else: - """ - This procedure could use more direct way of obtaining the broadcast IP - as it is really simple in Unix - ifconfig gives Bcast directly for example - or use something like : - iface = "eth0" - socket.inet_ntoa(fcntl.ioctl(socket.socket(socket.AF_INET, socket.SOCK_DGRAM), 35099, struct.pack('256s', iface))[20:24]) - """ - pattern = re.compile(r"(255.\d{1,3}.\d{1,3}.\d{1,3})") - + interfaces = netifaces.interfaces() + for nic in interfaces: + addresses = netifaces.ifaddresses(nic) try: - proc = subprocess.Popen("ifconfig", stdout=subprocess.PIPE) - while True: - line = proc.stdout.readline() - if ip.encode() in line: - break - mask = re.findall(pattern, line.decode())[0] - except: - mask = "255.255.255.255" - return mask + for address in addresses[netifaces.AF_INET]: + if address["addr"] == ip: + return address["netmask"] + except KeyError: + pass + return "255.255.255.255" def validate_ip_address(ip): diff --git a/requirements.txt b/requirements.txt index 9ada14ff..c44f2f91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ bacpypes>=0.18 +netifaces From 28c825fedf3a42a7d6f30ed3722e49c734529768 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Wed, 26 Aug 2020 14:44:01 -0400 Subject: [PATCH 23/27] Add statusFlags when creating point --- BAC0/core/devices/create_objects.py | 52 +++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/BAC0/core/devices/create_objects.py b/BAC0/core/devices/create_objects.py index 7734251f..0af7e178 100644 --- a/BAC0/core/devices/create_objects.py +++ b/BAC0/core/devices/create_objects.py @@ -21,7 +21,7 @@ from bacpypes.primitivedata import CharacterString, Date, Time, Real, Boolean from bacpypes.constructeddata import ArrayOf -from bacpypes.basetypes import EngineeringUnits, DateTime, PriorityArray +from bacpypes.basetypes import EngineeringUnits, DateTime, PriorityArray, StatusFlags from .mixins.CommandableMixin import LocalBinaryOutputObjectCmd @@ -47,6 +47,7 @@ def create_MV( numberOfStates=len(states), stateText=ArrayOf(CharacterString)(states), priorityArray=PriorityArray(), + statusFlags=StatusFlags(), ) msvo = _make_mutable(msvo, mutable=pv_writable) return msvo @@ -60,6 +61,7 @@ def create_AV(oid=1, pv=0, name="AV", units=None, pv_writable=False): units=units, relinquishDefault=0, priorityArray=PriorityArray(), + statusFlags=StatusFlags(), ) avo = _make_mutable(avo, mutable=pv_writable) avo = _make_mutable(avo, identifier="relinquishDefault", mutable=pv_writable) @@ -76,6 +78,7 @@ def create_BV( activeText=activeText, inactiveText=inactiveText, priorityArray=PriorityArray(), + statusFlags=StatusFlags(), ) bvo = _make_mutable(bvo, mutable=pv_writable) return bvo @@ -88,6 +91,7 @@ def create_AI(oid=1, pv=0, name="AI", units=None): presentValue=pv, units=units, outOfService=Boolean(False), + statusFlags=StatusFlags(), ) aio = _make_mutable(aio, identifier="outOfService", mutable=True) return aio @@ -100,6 +104,7 @@ def create_BI(oid=1, pv=0, name="BI", activeText="On", inactiveText="Off"): presentValue=pv, activeText=activeText, inactiveText=inactiveText, + statusFlags=StatusFlags(), ) @@ -110,6 +115,7 @@ def create_AO(oid=1, pv=0, name="AO", units=None, pv_writable=False): presentValue=pv, units=units, priorityArray=PriorityArray(), + statusFlags=StatusFlags(), ) aoo = _make_mutable(aoo, mutable=pv_writable) return aoo @@ -124,6 +130,7 @@ def create_BO( presentValue=pv, activeText=activeText, inactiveText=inactiveText, + statusFlags=StatusFlags(), ) boo = _make_mutable(boo, mutable=pv_writable) return boo @@ -131,7 +138,9 @@ def create_BO( def create_CharStrValue(oid=1, pv="null", name="String", pv_writable=False): charval = CharacterStringValueObject( - objectIdentifier=("characterstringValue", oid), objectName=name + objectIdentifier=("characterstringValue", oid), + objectName=name, + statusFlags=StatusFlags(), ) charval = _make_mutable(charval, mutable=pv_writable) charval.presentValue = CharacterString(pv) @@ -142,8 +151,45 @@ def create_DateTimeValue( oid=1, date=None, time=None, name="DateTime", pv_writable=False ): datetime = DateTimeValueObject( - objectIdentifier=("datetimeValue", oid), objectName=name + objectIdentifier=("datetimeValue", oid), + objectName=name, + statusFlags=StatusFlags(), ) datetime = _make_mutable(datetime, mutable=pv_writable) datetime.presentValue = DateTime(date=Date(date), time=Time(time)) return datetime + + +def create_object( + object_class, oid, objectName, description, presentValue=None, commandable=False +): + new_object = object_class( + objectIdentifier=(object_class.objectType, oid), + objectName="{}".format(objectName), + presentValue=presentValue, + description=CharacterString("{}".format(description)), + statusFlags=StatusFlags(), + ) + return _make_mutable(new_object, mutable=commandable) + + +def set_pv(obj=None, value=None, flags=[0, 0, 0, 0]): + obj.presentValue = value + obj.statusFlags = flags + + +def create_object_list(objects_dict): + """ + d = {name: (name, description, presentValue, units, commandable)} + """ + obj_list = [] + for obj_id, v in objects_dict.items(): + name, oid, description, presentValue, commandable = v + description = CharacterString(description) + new_obj = create_object( + object_class, name, oid, description, commandable=commandable + ) + if presentValue: + new_obj.presentValue = presentValue + obj_list.append(new_obj) + return obj_list From 62082e58d90238b18594bc1a0422d4acaea29dbc Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Wed, 26 Aug 2020 14:52:28 -0400 Subject: [PATCH 24/27] Add discoveredNetworks to get access to the list of networks found by discover --- BAC0/scripts/Base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BAC0/scripts/Base.py b/BAC0/scripts/Base.py index 403a9b05..2f1162b0 100644 --- a/BAC0/scripts/Base.py +++ b/BAC0/scripts/Base.py @@ -263,3 +263,7 @@ def _startAppThread(self): stopBacnetIPApp() self.t.join() raise + + @property + def discoveredNetworks(self): + return self.this_application.nse._learnedNetworks or set() From 5f145469a18e02bfee0720a1faa42852619e9de8 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Wed, 26 Aug 2020 14:53:58 -0400 Subject: [PATCH 25/27] Cleaning print statements --- BAC0/core/io/Write.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/BAC0/core/io/Write.py b/BAC0/core/io/Write.py index 37387b83..37267c42 100644 --- a/BAC0/core/io/Write.py +++ b/BAC0/core/io/Write.py @@ -144,10 +144,7 @@ def _validate_value_vs_datatype(self, obj_type, prop_id, indx, vendor_id, value) This will ensure the value can be encoded and is valid in the context """ # get the datatype - print(obj_type, prop_id, vendor_id) datatype = get_datatype(obj_type, prop_id, vendor_id=vendor_id) - print(datatype) - print(type(datatype)) # change atomic values into something encodeable, null is a special # case if value == "null": From 38cb633e57f2ba63dec33a6f2249baf2bc5e3268 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Wed, 26 Aug 2020 14:54:15 -0400 Subject: [PATCH 26/27] Bumping develop version --- BAC0/infos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BAC0/infos.py b/BAC0/infos.py index 34ba9c1f..8043bb10 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.08.03dev" +__version__ = "20.08.26dev" __license__ = "LGPLv3" From b0d53653edb433f8a7ce8342869327a123037a71 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing" Date: Wed, 26 Aug 2020 23:36:16 -0400 Subject: [PATCH 27/27] Attempt to create a routing_table property for bacnet instance. This is experimental and may change. Will ask if it's better just to use bacpypes function for that instead of creating a table myself in NetworkServiceElement --- BAC0/core/functions/Discover.py | 8 ++++---- BAC0/scripts/Base.py | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/BAC0/core/functions/Discover.py b/BAC0/core/functions/Discover.py index eb51680d..e2053104 100644 --- a/BAC0/core/functions/Discover.py +++ b/BAC0/core/functions/Discover.py @@ -68,6 +68,7 @@ def __init__(self): self._iartn = [] self._learnedNetworks = set() self.queue_by_address = {} + self._routing_table = {} def process_io(self, iocb): # get the destination address from the pdu @@ -124,11 +125,10 @@ def request(self, arg): def indication(self, adapter, npdu): if isinstance(npdu, IAmRouterToNetwork): if isinstance(self._request, WhoIsRouterToNetwork): - self._log.info( - "{} router to {}".format(npdu.pduSource, npdu.iartnNetworkList) - ) - address = str(npdu.pduSource) + address, netlist = str(npdu.pduSource), npdu.iartnNetworkList + self._log.info("{} router to {}".format(address, netlist)) self._iartn.append(address) + self._routing_table[address] = netlist for each in npdu.iartnNetworkList: self._learnedNetworks.add(int(each)) diff --git a/BAC0/scripts/Base.py b/BAC0/scripts/Base.py index 2f1162b0..2cb93179 100644 --- a/BAC0/scripts/Base.py +++ b/BAC0/scripts/Base.py @@ -267,3 +267,7 @@ def _startAppThread(self): @property def discoveredNetworks(self): return self.this_application.nse._learnedNetworks or set() + + @property + def routing_table(self): + return self.this_application.nse._routing_table or {}