From 6158c5ce4451cc80c46edd95be4a33f8046acac5 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing." Date: Fri, 13 Sep 2024 18:47:37 -0400 Subject: [PATCH 1/7] Adding use_router which leads to update_router_reference (after finding this router with who_is) --- BAC0/core/functions/Alias.py | 36 ++++++++++++++++++++++++++++++++++++ BAC0/scripts/Base.py | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/BAC0/core/functions/Alias.py b/BAC0/core/functions/Alias.py index 33f7c565..99b99f45 100644 --- a/BAC0/core/functions/Alias.py +++ b/BAC0/core/functions/Alias.py @@ -1,4 +1,5 @@ import asyncio +from typing import List, Tuple, Optional, Union from bacpypes3.app import Application from bacpypes3.pdu import Address, GlobalBroadcast @@ -7,6 +8,11 @@ from ...core.utils.notes import note_and_log +ROUTER_TUPLE_TYPE = Union[ + Tuple[Union[Address, str], Union[int, List[int]]], + Tuple[Union[Address, str], Union[int, List[int]], Optional[int]], +] + @note_and_log class Alias: @@ -110,6 +116,36 @@ async def init_routing_table(self, address): _app: Application = _this_application.app await _app.nse.initialize_routing_table() + async def use_router( + self, + router_infos: Union[ + Tuple[Union[Address, str], Union[int, List[int]]], + Tuple[Union[Address, str], Union[int, List[int]], Optional[int]], + ] = (None, None, None), + ): + address, dnets = router_infos[:2] + try: + snet = router_infos[2] + except IndexError: + snet = None + _this_application: BAC0Application = self.this_application + _app: Application = _this_application.app + if not isinstance(address, Address): + address = Address(address) + if not isinstance(dnets, list): + dnets = [dnets] + response = await self.who_is(address=address) + if response: + self._log.info(f"Router found at {address}") + self._log.info( + f"Adding router reference -> Snet : {snet} Addr : {address} dnets : {dnets}" + ) + await _app.nsap.update_router_references( + snet=snet, address=address, dnets=dnets + ) + else: + self._log.warning(f"Router not found at {address}") + async def what_is_network_number(self, destination=None, timeout=3): """ winn [ ] diff --git a/BAC0/scripts/Base.py b/BAC0/scripts/Base.py index 2ae1fc63..29efcef2 100644 --- a/BAC0/scripts/Base.py +++ b/BAC0/scripts/Base.py @@ -78,7 +78,7 @@ class Base: def __init__( self, - localIPAddr: str = "127.0.0.1", + localIPAddr: Address = Address("127.0.0.1/24"), networkNumber: int = None, localObjName: str = "BAC0", deviceId: int = None, From 19f8d5746bf9d0a91b4dc42d0d4a832c08a5c221 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing." Date: Fri, 13 Sep 2024 18:48:44 -0400 Subject: [PATCH 2/7] dev version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 41ca5cf6..209d7490 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "BAC0" -version = "2024.09.08" +version = "2024.09.13dev" description = "BACnet Scripting Framework for testing DDC Controls" authors = [{name = "Christian Tremblay", email = "christian.tremblay@servisys.com"}] readme = "README.md" From 66310aa6ce68b996b2b50d20be64a51c21c25613 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing." Date: Sun, 15 Sep 2024 13:50:22 -0400 Subject: [PATCH 3/7] test with rejectMessage --- BAC0/core/functions/Alias.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/BAC0/core/functions/Alias.py b/BAC0/core/functions/Alias.py index 33f7c565..6cb050dc 100644 --- a/BAC0/core/functions/Alias.py +++ b/BAC0/core/functions/Alias.py @@ -1,6 +1,7 @@ import asyncio from bacpypes3.app import Application +from bacpypes3.npdu import RejectMessageToNetwork from bacpypes3.pdu import Address, GlobalBroadcast from BAC0.core.app.asyncApp import BAC0Application @@ -126,6 +127,8 @@ async def what_is_network_number(self, destination=None, timeout=3): _app.nse.what_is_network_number(), timeout ) return network_number + except RejectMessageToNetwork as error: + self._log.warning(f"Reject Network Number : {error}") except asyncio.TimeoutError: # Handle the timeout error self.log( From cc69ef787a7deb79c28c168e4fa0d62c154428a7 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing." Date: Sun, 15 Sep 2024 14:22:48 -0400 Subject: [PATCH 4/7] Improving discover to be compatible with the new feature use_router that allows to manually add a router to the app (this fix the troubles I have connecting to network using the CWT from JCI) --- BAC0/core/functions/Alias.py | 15 ++++++++++++--- BAC0/core/functions/Discover.py | 3 ++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/BAC0/core/functions/Alias.py b/BAC0/core/functions/Alias.py index 81bc3e31..bd729b9f 100644 --- a/BAC0/core/functions/Alias.py +++ b/BAC0/core/functions/Alias.py @@ -1,7 +1,8 @@ import asyncio -from typing import List, Tuple, Optional, Union +from typing import List, Optional, Tuple, Union from bacpypes3.app import Application +from bacpypes3.netservice import RouterEntryStatus from bacpypes3.npdu import RejectMessageToNetwork from bacpypes3.pdu import Address, GlobalBroadcast @@ -144,6 +145,15 @@ async def use_router( await _app.nsap.update_router_references( snet=snet, address=address, dnets=dnets ) + self._ric = self.this_application.app.nsap.router_info_cache + self._log.info( + f"Updating router info cache -> Snet : {snet} Addr : {address} dnets : {dnets}" + ) + for each in dnets: + await self._ric.set_path_info( + snet, each, address, RouterEntryStatus.available + ) + _this_application._learnedNetworks.add(each) else: self._log.warning(f"Router not found at {address}") @@ -163,8 +173,7 @@ async def what_is_network_number(self, destination=None, timeout=3): _app.nse.what_is_network_number(), timeout ) return network_number - except RejectMessageToNetwork as error: - self._log.warning(f"Reject Network Number : {error}") + except asyncio.TimeoutError: # Handle the timeout error self.log( diff --git a/BAC0/core/functions/Discover.py b/BAC0/core/functions/Discover.py index d46ff67d..9175e2ed 100644 --- a/BAC0/core/functions/Discover.py +++ b/BAC0/core/functions/Discover.py @@ -84,11 +84,12 @@ async def _discover( if reset: self.discoveredDevices = {} found = [] - _networks = [] _this_application: BAC0Application = self.this_application _app: Application = _this_application.app + _networks = _this_application._learnedNetworks + deviceInstanceRangeLowLimit, deviceInstanceRangeHighLimit = limits # Try to find on which network we are _this_network = await self.what_is_network_number() From 642a41fafa12d1a9aa850c2a5c36680d38470845 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing." Date: Sun, 15 Sep 2024 20:04:00 -0400 Subject: [PATCH 5/7] missing destination on init_routing_table --- BAC0/core/functions/Alias.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/BAC0/core/functions/Alias.py b/BAC0/core/functions/Alias.py index bd729b9f..4e887512 100644 --- a/BAC0/core/functions/Alias.py +++ b/BAC0/core/functions/Alias.py @@ -105,7 +105,7 @@ async def whois_router_to_network( ) return [] - async def init_routing_table(self, address): + async def init_routing_table(self, address=None): """ irt @@ -116,7 +116,9 @@ async def init_routing_table(self, address): self.log(f"Addr : {address}", level="info") _this_application: BAC0Application = self.this_application _app: Application = _this_application.app - await _app.nse.initialize_routing_table() + if address is not None and not isinstance(address, Address): + address = Address(address) + await _app.nse.initialize_routing_table(destination=address) async def use_router( self, From bf1cbde7f6512636c29242369fa95108277048f3 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing." Date: Sun, 15 Sep 2024 21:09:19 -0400 Subject: [PATCH 6/7] migrating and improving manual_test_create_device leads to improving write REGEX --- BAC0/core/io/Write.py | 2 +- tests/manual_test_create_device.py | 98 +++++++++++++++++++----------- 2 files changed, 62 insertions(+), 38 deletions(-) diff --git a/BAC0/core/io/Write.py b/BAC0/core/io/Write.py index 1c5edf71..f46f3bb7 100644 --- a/BAC0/core/io/Write.py +++ b/BAC0/core/io/Write.py @@ -49,7 +49,7 @@ def write() # some debugging _debug = 0 _LOG = ModuleLogger(globals()) -WRITE_REGEX = r"(?P
\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b|(\b\d+:\d+\b)) (?P(@obj_)?[-\w:]*[: ]*\d*) (?P(@prop_)?\w*(-\w*)?)[ ]?(?P-*\w*)?[ ]?(?P-|\d*)?[ ]?(?P(1[0-6]|[0-9]))?" +WRITE_REGEX = r"(?P
\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?::\d+)?\b|(\b\d+:\d+\b)) (?P(@obj_)?[-\w:]*[: ]*\d*) (?P(@prop_)?\w*(-\w*)?)[ ]?(?P-*\w*)?[ ]?(?P-|\d*)?[ ]?(?P(1[0-6]|[0-9]))?" write_pattern = re.compile(WRITE_REGEX) diff --git a/tests/manual_test_create_device.py b/tests/manual_test_create_device.py index 9039896c..66102e96 100644 --- a/tests/manual_test_create_device.py +++ b/tests/manual_test_create_device.py @@ -1,4 +1,5 @@ import asyncio +import random import BAC0 from BAC0.core.devices.local.factory import ( @@ -14,7 +15,11 @@ make_state_text, multistate_input, multistate_output, + multistate_value, ) +from BAC0.scripts.script_runner import run + +bacnet = None def add_points(qty_per_type, device): @@ -36,17 +41,17 @@ def add_points(qty_per_type, device): ) states = make_state_text(["Normal", "Alarm", "Super Emergency"]) - # _new_objects = multistate_value( - # description="An Alarm Value", - # properties={"stateText": states}, - # name="BIG-ALARM", - # is_commandable=False, - # ) + _new_objects = multistate_value( + description="An Alarm Value", + properties={"stateText": states}, + name="BIG-ALARM", + is_commandable=False, + ) # All others using default implementation for _ in range(qty_per_type): # _new_objects = analog_output(presentValue=89.9) - _new_objects = analog_value(presentValue=79.9) + _new_objects = analog_value(presentValue=79.9, is_commandable=True) _new_objects = binary_input() _new_objects = binary_output() _new_objects = binary_value() @@ -60,36 +65,55 @@ def add_points(qty_per_type, device): async def main(): - bacnet = BAC0.lite() - - # We'll use 3 devices with our first instance - device_app = BAC0.lite(port=47809, deviceId=101, localObjName="App1") - device30_app = BAC0.lite(port=47810, deviceId=102, localObjName="App2") - device300_app = BAC0.lite(port=47811, deviceId=103, localObjName="App3") - - add_points(2, device_app) - add_points(3, device30_app) - add_points(4, device300_app) - - ip = device_app.localIPAddr.addrTuple[0] - boid = device_app.Boid - - ip_30 = device30_app.localIPAddr.addrTuple[0] - boid_30 = device30_app.Boid - - ip_300 = device300_app.localIPAddr.addrTuple[0] - boid_300 = device300_app.Boid - - # Connect to test device using main network - test_device = BAC0.device("{}:47809".format(ip), boid, bacnet, poll=10) - test_device_30 = BAC0.device("{}:47810".format(ip_30), boid_30, bacnet, poll=0) - test_device_300 = BAC0.device("{}:47811".format(ip_300), boid_300, bacnet, poll=0) - while True: - await asyncio.sleep(0.01) - # print(test_device.points) - # (test_device_30.points) - # (test_device_300.points) + # We'll use 3 devices plus our main instance + async with BAC0.start(localObjName="bacnet") as bacnet: + async with BAC0.start(port=47809, localObjName="App1") as device_app: + async with BAC0.start(port=47810, localObjName="App2") as device30_app: + async with BAC0.start(port=47811, localObjName="App3") as device300_app: + add_points(2, device_app) + add_points(3, device30_app) + add_points(4, device300_app) + + ip = device_app.localIPAddr.addrTuple[0] + boid = device_app.Boid + + ip_30 = device30_app.localIPAddr.addrTuple[0] + boid_30 = device30_app.Boid + + ip_300 = device300_app.localIPAddr.addrTuple[0] + boid_300 = device300_app.Boid + + # Connect to test device using main network + test_device = await BAC0.device( + f"{ip}:47809", boid, bacnet, poll=10 + ) + test_device_30 = await BAC0.device( + f"{ip_30}:47810", boid_30, bacnet, poll=0 + ) + test_device_300 = await BAC0.device( + f"{ip_300}:47811", boid_300, bacnet, poll=0 + ) + bacnet._log.info("CTRL-C to exit") + + while True: + await asyncio.sleep(2) + bacnet._log.info(f"{test_device['BIG-ALARM']}") + new_val_for_av1 = random.randint(1, 100) + bacnet._log.info(f"Setting AV-1 to {new_val_for_av1}") + # test_device_30['AV-1'] = new_val_for_av1 + await test_device_30["AV-1"].write(new_val_for_av1, priority=16) + await asyncio.sleep(2) + bacnet._log.info( + f"Forcing a read on test_device_30/AV-1 : {await test_device_30['AV-1'].value}" + ) + test_device_300["AV-1"] = new_val_for_av1 + await asyncio.sleep(2) + bacnet._log.info( + f"Forcing a read on test_device_300/AV-1 : {await test_device_300['AV-1'].value}" + ) + # (test_device_300.points) if __name__ == "__main__": - asyncio.run(main()) + run(main, bacnet) + # asyncio.run(main()) From 71d504fff7184824804647226e0a4e074ffbede8 Mon Sep 17 00:00:00 2001 From: "Christian Tremblay, ing." Date: Sun, 15 Sep 2024 21:33:24 -0400 Subject: [PATCH 7/7] Migrated and improved manual_test_cov which leads to including a default value of 0.15 in covIncrment for analog values in the factory... we don't want None there as it leads to errors when someone register --- BAC0/core/devices/local/factory.py | 2 +- tests/manual_test_cov.py | 40 ++++++++++++++++++++++++++++++ tests/manualtest_cov.py | 27 -------------------- 3 files changed, 41 insertions(+), 28 deletions(-) create mode 100644 tests/manual_test_cov.py delete mode 100644 tests/manualtest_cov.py diff --git a/BAC0/core/devices/local/factory.py b/BAC0/core/devices/local/factory.py index fa6be587..31dc330f 100644 --- a/BAC0/core/devices/local/factory.py +++ b/BAC0/core/devices/local/factory.py @@ -328,7 +328,7 @@ def analog(**kwargs): "properties": { "units": "percent", # "eventState": EventState(), - # "covIncrement": 0.15, + "covIncrement": 0.15, }, "presentValue": 0, } diff --git a/tests/manual_test_cov.py b/tests/manual_test_cov.py new file mode 100644 index 00000000..fd0032b0 --- /dev/null +++ b/tests/manual_test_cov.py @@ -0,0 +1,40 @@ +import asyncio +import random + +from bacpypes3.primitivedata import Real + +import BAC0 +from BAC0.core.devices.local.factory import analog_value +from BAC0.scripts.script_runner import run + +bacnet = None + + +async def main(): + async with BAC0.start() as bacnet: + device = BAC0.start(port=47809, deviceId=123) + + new_obj = analog_value(presentValue=0) + new_obj.add_objects_to_application(device) + + # From Server + dev_av = device.this_application.app.get_object_name("AV") + print(dev_av.covIncrement) + + # From client + ip = device.localIPAddr.addrTuple[0] + boid = device.Boid + bacnet._log.info("Defining device with poll 0 so the AV won't get updated") + dev = await BAC0.device(f"{ip}:47809", boid, bacnet, poll=0) + av = dev["AV"] + bacnet._log.info("Subscribing to AV") + await dev["AV"].subscribe_cov(lifetime=90) + + while True: + dev_av.presentValue = Real(random.randint(1, 100)) + bacnet._log.info(f"Setting AV to {dev_av.presentValue}") + await asyncio.sleep(5) + + +if __name__ == "__main__": + run(main, bacnet) diff --git a/tests/manualtest_cov.py b/tests/manualtest_cov.py deleted file mode 100644 index c5dc00c2..00000000 --- a/tests/manualtest_cov.py +++ /dev/null @@ -1,27 +0,0 @@ -import time - - -import BAC0 -from BAC0.core.devices.local.models import analog_value - -device = BAC0.lite("192.168.211.55/24", deviceId=123) -client = BAC0.lite("192.168.212.12/24") - -new_obj = analog_value() -new_obj.add_objects_to_application(device) - -# From Server -dev_av = device.this_application.get_object_name("AV") -print(dev_av.covIncrement) - -# From client -dev = dev = BAC0.device("192.168.211.55", 123, client, poll=0) -av = dev["AV"] -dev["AV"].subscribe_cov(lifetime=0) - -print() -while True: - print(dev["AV"]) - time.sleep(1) - dev["AV"] += 1 - time.sleep(1)