Skip to content

Commit b53acc9

Browse files
committed
fixing bacnet
1 parent ddc0295 commit b53acc9

File tree

5 files changed

+72
-34
lines changed

5 files changed

+72
-34
lines changed

buildingmotif/ingresses/bacnet.py

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# configure logging output
2+
import asyncio
23
import logging
34
import warnings
45
from functools import cached_property
56
from typing import Any, Dict, List, Optional, Tuple
67

78
try:
89
import BAC0
9-
from BAC0.core.devices.Device import Device as BACnetDevice
1010
except ImportError:
1111
logging.critical(
1212
"Install the 'bacnet-ingress' module, e.g. 'pip install buildingmotif[bacnet-ingress]'"
@@ -23,43 +23,75 @@
2323

2424

2525
class BACnetNetwork(RecordIngressHandler):
26-
def __init__(self, ip: Optional[str] = None):
26+
def __init__(
27+
self,
28+
ip: Optional[str] = None,
29+
*,
30+
discover_kwargs: Optional[Dict[str, Any]] = None,
31+
ping: bool = False,
32+
device_kwargs: Optional[Dict[str, Any]] = None,
33+
):
2734
"""
2835
Reads a BACnet network to discover the devices and objects therein
2936
30-
:param ip: IP/mask for the host which is canning the networks,
37+
:param ip: IP/mask for the host which is scanning the network,
3138
defaults to None
3239
:type ip: Optional[str], optional
40+
:param discover_kwargs: Optional kwargs forwarded to BAC0._discover.
41+
:type discover_kwargs: Optional[Dict[str, Any]]
42+
:param ping: Whether to ping devices during connect; defaults to False.
43+
:type ping: bool
44+
:param device_kwargs: Optional kwargs forwarded to BAC0.device.
45+
:type device_kwargs: Optional[Dict[str, Any]]
3346
"""
34-
# create the network object; this will handle scans
35-
# Be a good net citizen: do not ping BACnet devices
36-
self.network = BAC0.connect(ip=ip, ping=False)
37-
# initiate discovery of BACnet networks
38-
self.network.discover()
39-
40-
self.devices: List[BACnetDevice] = []
41-
self.objects: Dict[Tuple[str, int], List[dict]] = {}
42-
43-
# for each discovered Device, create a BAC0.device object
44-
# This will read the BACnet objects off of the Device.
45-
# Save the BACnet objects in the objects dictionary
47+
self.objects: Dict[Tuple[str, int], List[Dict[str, Any]]] = {}
48+
self._run_async(
49+
self._collect_objects(
50+
ip=ip,
51+
discover_kwargs=discover_kwargs or {},
52+
ping=ping,
53+
device_kwargs=device_kwargs or {},
54+
)
55+
)
56+
57+
def _run_async(self, coro):
58+
loop = asyncio.new_event_loop()
4659
try:
47-
if self.network.discoveredDevices is None:
60+
asyncio.set_event_loop(loop)
61+
loop.run_until_complete(coro)
62+
loop.run_until_complete(loop.shutdown_asyncgens())
63+
finally:
64+
asyncio.set_event_loop(None)
65+
loop.close()
66+
67+
async def _collect_objects(
68+
self,
69+
*,
70+
ip: Optional[str],
71+
discover_kwargs: Dict[str, Any],
72+
ping: bool,
73+
device_kwargs: Dict[str, Any],
74+
):
75+
device_kwargs.setdefault("poll", 0)
76+
77+
async with BAC0.start(ip=ip, ping=ping) as bacnet:
78+
await bacnet._discover(**discover_kwargs)
79+
80+
discovered = getattr(bacnet, "discoveredDevices", None)
81+
if not discovered:
4882
warnings.warn("BACnet ingress could not find any BACnet devices")
49-
for (address, device_id) in self.network.discoveredDevices: # type: ignore
50-
# set poll to 0 to avoid reading the points regularly
51-
dev = BAC0.device(address, device_id, self.network, poll=0)
52-
self.devices.append(dev)
53-
self.objects[(address, device_id)] = []
83+
return
84+
85+
for (address, device_id) in discovered:
86+
device = await BAC0.device(address, device_id, bacnet, **device_kwargs)
87+
objects: List[Dict[str, Any]] = []
5488

55-
for bobj in dev.points:
89+
for bobj in device.points:
5690
obj = bobj.properties.asdict
5791
self._clean_object(obj)
58-
self.objects[(address, device_id)].append(obj)
59-
finally:
60-
for dev in self.devices:
61-
self.network.unregister_device(dev)
62-
self.network.disconnect()
92+
objects.append(obj)
93+
94+
self.objects[(address, device_id)] = objects
6395

6496
def _clean_object(self, obj: Dict[str, Any]):
6597
if "name" in obj:

docs/guides/Dockerfile.bacnet

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ WORKDIR /opt
44

55
RUN apt update && apt install -y python3 python3-pip && rm -rf /var/lib/apt/lists/*
66

7-
RUN pip3 install BACpypes
7+
RUN pip3 install BACpypes --break-system-packages
88

99
COPY virtual_bacnet.py virtual_bacnet.py
1010
COPY BACpypes.ini .

docs/guides/csv-import.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ tstat2,room345,co2-345,temp-345,sp-345
6767
tstat3,room567,cow-567,temp-567,sp-567
6868
```
6969

70-
We can create a CSV ingress handler using the built-in class ([`CSVIngressHandler`](/reference/apidoc/_autosummary/buildingmotif.ingresses.csv.html#buildingmotif.ingresses.csv.CSVIngress)):
70+
We can create a CSV ingress handler using the built-in class ([`CSVIngressHandler`](/reference/apidoc/_autosummary/buildingmotif.ingresses.csvingress.html#buildingmotif.ingresses.csvingress.CSVIngress)):
7171

7272
```python
7373
from rdflib import Namespace, Graph

docs/guides/docker-compose-bacnet.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
version: "3.4"
21
services:
32
device:
43
build:
@@ -13,4 +12,4 @@ networks:
1312
driver: default
1413
config:
1514
- subnet: "172.24.0.0/16"
16-
gateway: "172.24.0.1"
15+
gateway: "172.24.0.1"

docs/guides/ingress-bacnet-to-brick.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ RUN apt update \
119119
python3-pip \
120120
&& rm -rf /var/lib/apt/lists/*
121121
122-
RUN pip3 install BACpypes
122+
RUN pip3 install BACpypes --break-system-packages
123123
124124
COPY virtual_bacnet.py virtual_bacnet.py
125125
COPY BACpypes.ini .''')
@@ -188,12 +188,19 @@ We use the `buildingmotif.ingresses.bacnet.BACnetNetwork` ingress module to pull
188188
```{code-cell} python3
189189
from buildingmotif.ingresses.bacnet import BACnetNetwork
190190
191-
bacnet = BACnetNetwork("172.24.0.1/32") # don't change this if you are using the docker compose setup
191+
bacnet = BACnetNetwork(
192+
"172.24.0.1/32",
193+
discover_kwargs={"global_broadcast": True}, # optional, helps find virtual devices
194+
)
192195
for rec in bacnet.records:
193196
print(rec)
194197
```
195198

196-
Each of these records has an `rtype` field, which is used by the ingress implementation to differentiate between different kinds of records; here it differentiates between BACnet Devices and BACnet Objects, which have different expressions in Brick. The `fields` attribute cotnains arbitrary key-value pairs, again defined by the ingress implementation, which can be interpreted by another ingress module.
199+
```{note}
200+
The `BACnetNetwork` ingress uses BAC0's asynchronous connection helpers under the hood. It opens the network with `BAC0.start` and blocks on discovery by awaiting `bacnet._discover(**discover_kwargs)`, so forwarding options such as `global_broadcast` or `whois` will influence how aggressively the underlying BACnet scan runs. No additional event-loop management is needed when calling the ingress from synchronous code.
201+
```
202+
203+
Each of these records has an `rtype` field, which is used by the ingress implementation to differentiate between different kinds of records; here it differentiates between BACnet Devices and BACnet Objects, which have different expressions in Brick. The `fields` attribute contains arbitrary key-value pairs, again defined by the ingress implementation, which can be interpreted by another ingress module.
197204

198205
## BACnet to Brick: an Initial Model
199206

0 commit comments

Comments
 (0)