ChristianTremblay / BAC0

BAC0 - Library depending on BACpypes3 (Python 3) to build automation script for BACnet applications
GNU Lesser General Public License v3.0
174 stars 99 forks source link

Ansyc branch #447

Closed bbartling closed 7 months ago

bbartling commented 7 months ago

Hello,

Am on Windows with a fresh version of Python 3.12.2, git cloned the BAC0 repo, check out async branch, pip install .

Just to double check:

$ git status
On branch async
Your branch is up to date with 'origin/async'.

nothing to commit, working tree clean

And with this script which I think would have worked just fine on the older non async BAC0 version:

import BAC0
import time

bacnet = BAC0.lite()
address = '12345:2'

sensor_object_type = 'analogInput'
sensor_object_instance = '2'

cmd_object_type = 'analogValue'
cmd_object_instance = '302'
priority = 10 

while True:

    # MSTP device hardware address 2 on MSTP network 12345
    read_sensor_str = f'{address} {sensor_object_type} {sensor_object_instance} presentValue'
    print("Executing read_sensor_str statement:", read_sensor_str)
    sensor = bacnet.read(read_sensor_str)
    print("sensor: ",sensor)

    time.sleep(60)

bacnet.disconnect()

Trace back:

Traceback (most recent call last):
  File "C:\Users\bbartling\OneDrive - Slipstream\Desktop\baco_testing\read_request.py", line 1, in <module>
    import BAC0
  File "C:\Users\bbartling\AppData\Local\Programs\Python\Python312\Lib\site-packages\BAC0\__init__.py", line 17, in <module>
    from .core.devices.Device import DeviceLoad as load
  File "C:\Users\bbartling\AppData\Local\Programs\Python\Python312\Lib\site-packages\BAC0\core\devices\Device.py", line 86, in <module>
    class Device(SQLMixin):
  File "C:\Users\bbartling\AppData\Local\Programs\Python\Python312\Lib\site-packages\BAC0\core\devices\Device.py", line 228, in Device
    def df(self, list_of_points: List[str], force_read: bool = True) -> pd.DataFrame:
NameError: name 'pd' is not defined. Did you mean: 'id'?

If I pip install Pandas on py 3.12 and test again, this is the full trace back:

= RESTART: C:\Users\bbartling\OneDrive - Slipstream\Desktop\baco_testing\read_request.py
2024-03-12 09:11:59,964 - INFO    | Starting BAC0 version 2024 (Lite)
2024-03-12 09:12:00,024 - INFO    | Use BAC0.log_level to adjust verbosity of the app.
2024-03-12 09:12:00,028 - INFO    | Ex. BAC0.log_level('silence') or BAC0.log_level('error')
Traceback (most recent call last):
  File "C:\Users\bbartling\OneDrive - Slipstream\Desktop\baco_testing\read_request.py", line 4, in <module>
    bacnet = BAC0.lite()
  File "C:\Users\bbartling\AppData\Local\Programs\Python\Python312\Lib\site-packages\BAC0\scripts\Lite.py", line 128, in __init__
    self._ping_task.start()
  File "C:\Users\bbartling\AppData\Local\Programs\Python\Python312\Lib\site-packages\BAC0\tasks\TaskManager.py", line 129, in start
    self.aio_task = asyncio.create_task(self.execute(), name=f"aio{self.name}")
  File "C:\Users\bbartling\AppData\Local\Programs\Python\Python312\Lib\asyncio\tasks.py", line 417, in create_task
    loop = events.get_running_loop()
RuntimeError: no running event loop

Let me know if I can tinker around with the code and make a PR? Maybe Pandas is a neat idea to keep to build up dataframes of like cached read request data... any thoughts?

bbartling commented 7 months ago

Hi @ChristianTremblay, hows this looking?

import BAC0
import asyncio
from signal import SIGINT, SIGTERM

def read_sensor_value(address, sensor_object_type, sensor_object_instance):
    bacnet = BAC0.lite()

    try:
        # Formulate the read request string
        read_sensor_str = f'{address} {sensor_object_type} {sensor_object_instance} presentValue'
        print("Executing read_sensor_str statement:", read_sensor_str)

        # Perform the read operation
        sensor_value = bacnet.read(read_sensor_str)
        print("Sensor Value: ", sensor_value)

    finally:
        # Ensure the BACnet connection is properly closed
        bacnet.disconnect()

    # Return the sensor value to the calling function
    return sensor_value

if __name__ == "__main__":
    loop = asyncio.get_event_loop()

    for sig in (SIGINT, SIGTERM):
        loop.add_signal_handler(sig, handler)

    loop.create_task(main())
    loop.run_forever()

    tasks = asyncio.all_tasks(loop=loop)
    for t in tasks:
        t.cancel()
    group = asyncio.gather(*tasks, return_exceptions=True)
    loop.run_until_complete(group)
    loop.close()
ChristianTremblay commented 7 months ago

Not sure why I would do this, but, let say it is a proof of concept :-)

import BAC0
import asyncio
from signal import SIGINT, SIGTERM, signal
import  click

bacnet = None
loop = None

async def get_units(address:str = None, object_type:str = None, object_instance:str = None) -> str:
    units = await bacnet.read(f"{address} {object_type} {object_instance} units")
    return units

@click.command()
@click.option('--address', default="2:5", help='BACnet device address')
@click.option('--object_type', default="analogInput", help='BACnet object type')
@click.option('--object_instance', default="1022", help='BACnet object instance')
def main(address, object_type, object_instance):
    global loop
    loop = asyncio.get_event_loop()
    #for sig in (SIGINT, SIGTERM):
    #    loop.add_signal_handler(sig, handler)
    signal(SIGINT, handler)
    signal(SIGTERM, handler)

    loop.create_task(_main(address, object_type, object_instance))
    loop.run_forever()
    tasks = asyncio.all_tasks(loop=loop)
    for t in tasks:
        t.cancel()
    group = asyncio.gather(*tasks, return_exceptions=True)
    loop.run_until_complete(group)
    loop.close()

async def _main(address:str = None, object_type:str = None, object_instance:str = None) -> None:
    global bacnet
    bacnet = BAC0.lite()
    while True:
        val = await bacnet.read(f"{address} {object_type} {object_instance} presentValue")
        units = await get_units(address, object_type, object_instance)
        print(f"Sensor Value: {val} {units}...waiting 5 seconds")
        await asyncio.sleep(5)

def handler(sig, sig2):
    print(f"Got signal: {sig!s}, shutting down.")
    bacnet.disconnect()
    loop.stop()
    #loop.remove_signal_handler(SIGTERM)
    #loop.add_signal_handler(SIGINT, lambda: None)

if __name__ == "__main__":
    main()
ChristianTremblay commented 7 months ago

Be sure to pull, I made some modification to handle disocnnection better

It gives me this on Windows :

image

bbartling commented 7 months ago

would you ever recommend to just using IPython for BAC0? Sort of like this YouTube video series which is pretty cool BTW for BACnet tutorial they demo'd BAC0: https://youtu.be/HOapQkCxCHk?si=s7Yw7r9czL9ZnDcg

ChristianTremblay commented 7 months ago

I wasn't aware of those videos.

I mostly use two use cases

IPython REPL was my favorite because of tab completion and Colors but with bacpypes3 I haven't been able to make it work. My lack of knowledge in async/await world is pretty annoying...

This is why now I do all the development in Notebook (inside VSCode). There is an event loop that already run and it just work.

And I like to be able to test things on the fly.

bbartling commented 7 months ago

cool stuff thanks Christian.

ChristianTremblay commented 7 months ago

@bbartling I don't know it is easier... but I kinda like the fact that a context manager will deal with all disconnections...

import BAC0
import asyncio
from signal import SIGINT, SIGTERM, signal
import click

loop = None
fini = False

async def get_units(
    bacnet, address: str = None, object_type: str = None, object_instance: str = None
) -> str:
    units = await bacnet.read(f"{address} {object_type} {object_instance} units")
    return units

@click.command()
@click.option("--ip", default="", help="Interface IP address with subnet mask ")
@click.option("--address", default="2:5", help="BACnet device address")
@click.option("--object_type", default="analogInput", help="BACnet object type")
@click.option("--object_instance", default="1022", help="BACnet object instance")
def main(ip, address, object_type, object_instance):
    global loop
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
    signal(SIGINT, handler)
    signal(SIGTERM, handler)

    loop.create_task(_main(ip, address, object_type, object_instance))
    loop.run_forever()
    tasks = asyncio.all_tasks(loop=loop)
    for t in tasks:
        t.cancel()
    group = asyncio.gather(*tasks, return_exceptions=True)
    loop.run_until_complete(group)
    loop.close()

async def _main(
    ip:str, address: str = None, object_type: str = None, object_instance: str = None
) -> None:
    global bacnet
    global fini
    async with BAC0.lite(ip=ip) as bacnet:
        #await bacnet._discover()
        while not fini:
            val = await bacnet.read(
            f"{address} {object_type} {object_instance} presentValue"
            )
            units = await get_units(bacnet, address, object_type, object_instance)
            print(f"Sensor Value: {val} {units}...waiting 5 seconds")
            await asyncio.sleep(5)

def handler(sig, sig2):
    global fini
    fini = True
    print(f"Got signal: {sig!s}, shutting down.")
    loop.stop()

if __name__ == "__main__":
    main()
bbartling commented 7 months ago

Looks great thanks