Package: digimat.bac0
Python digimat.bac0digimat.bac0is a Python 3 module basically providing a wrapper around the excellentBAC0module,
itself usingBACpypes. This module provide super easy access (browsing, interacting) to BACnet/IP networks. This module can be used interactively or within an application. Install this module with apip install -U digimat.bac0command.>>>fromdigimat.bac0importBAC>>>bacnet=BAC(network='192.168.0.84/24')Congratulations ! You just have launched a BACnet/IP node binded to your LAN card (ip 192.168.0.84, netmask 255.255.255.0). This shortYoutube Screencastshows a small interactive usage of this module. You have to provide the ip (“ip/networkSize”) of the card that will be used to send+receive every BACnet UDP messages. Under Linux/MacOS hosts, this can be done automagically if you have installed thenetifacesmodule (install it withpip install -U netifaces). If so, you can ommit thenetworkparameter (the module willtryto guess the ip address of your network card)>>>bacnet=BAC()# return a BAC object>>>bacnet<BAC:192.168.0.84/24(0devices)>If needed, you can give abbmd router(‘ipAddress[:port]’) to the constructor>>>bacnet=BAC(router='192.168.2.1')Now, assuming that your network settings are correct, you can start discovering the BACnet network>>>bacnet.whois()# return a list of devices detected on the network[('192.168.0.15',8015),('192.168.0.80',4000)]>>>bacnet.whois('*:*')# by default, send the request on all networks>>>bacnet.whois('2001:*')# search devices on the 2001 network onlyThewhois()method returns a list of remote BACnet/address servers detected on the network (address/deviceId). As most of theses BACnet requests are using BAC0/BACpypes requests, you will find additional information in the documentation of theses projects (if really needed). To communicate with BACnet devices (BACnet servers), you can add (declare) to them your node>>>device=bacnet.declareDevice(8015,'192.168.0.15')>>>device=bacnet.declareDevice(did=8015,address='192.168.0.15',poll=60)# with polling time specified (default is to poll it every 15s)In fact, if the device is detected by the whois(), you can declare a node based on it’s id or it’s address. A whois() request will be triggered to retrieve the associated device missing address/id value. This will create (if not already done) and return the corresponding BACDevice object.>>>device=bacnet.declareDeviceFromId(8015)# return a BACDevice object>>>device=bacnet.declareDeviceFromAddress('192.168.0.15')# equivalent>>>device<BACDevice:8015:0(Digimat:Digimat3_CPU_Bridge),operational,66points)>If you are super lazy (no you’re not), you can even use the .discover() method to auto declare every device discovered by a whois() request. But you can’t really predict what will be exactly done, so it’s more an helper for interactive sessions.>>>bacnet.discover()# this will magically declare every device reported by whois()The underlying BAC0 module has started a thread managing each remote bacnet device (understand remote “BACnet servers”) you will declare to your node. Every BAC* object (BAC, BACDevice, BACPoints, BACPoint) has a .dump() method, very useful in interactive sessions>>>bacnet.dump()+---+---------+------+--------------+---------+---------------------+-------------+-------------+---------+|# | name    |  id  |   address    | vendor  | model               |    status   | description | #points |+---+---------+------+--------------+---------+---------------------+-------------+-------------+---------+|0|s_112_1|8015|192.168.0.15|Digimat|Digimat3_CPU_Bridge|operational|S+T|66|+---+---------+------+--------------+---------+---------------------+-------------+-------------+---------+>>>device.dump()+-----------------------+--------------+|property|value|+-----------------------+--------------+|ip|192.168.0.15||address|8015||name|s_112_1||description|S+T||systemStatus|operational||vendorName|Digimat||vendorIdentifier|892||points|66||segmentationSupported|True||analogInput|16||analogOutput|8||binaryInput|31||binaryOutput|11|+-----------------------+--------------+Once a device has been declared, you can retrieve a reference to the corresponding BACDevice object it with bacnet.device(…) or directly with a bacnet[…] array access. You can use either the index (0), the name (s_112_1), the id (8105) or the address (192.168.0.15) to retrieve your device from the BAC object. If you redeclare a device already existing, it will be simply returns the existing one (no duplication). The BAC object is iterable, so you can also use something like “for device in BAC:”. requesting a non existant (non declared) device returns None.You will have to dig a bit into theBACandBACDeviceobjects to find avalaible methods and properties. It’s now time to access to the points (variables) of our device, all provided by the device.points property, returning aBACPointsobject>>>points=device.points>>>points<BACPoints(66points)>>>>points.dump()+----+---------------------+-------------------------------------------------------------------------+-------------------+---------+--------------+-----+-------+-------+|#  | name                | description                                                             | descriptor        |   value | label        | age |  COV  |  OoS  |+----+---------------------+-------------------------------------------------------------------------+-------------------+---------+--------------+-----+-------+-------+|0|r_112_1_cio_13056_0|sondeexterieure|analogInput13056|25.5573|C|4s|False|False||1|r_112_1_cio_13057_0|sondedepartchaudiere|analogInput13057|25.0004|C|4s|False|False||2|r_112_1_cio_13058_0|sondedepartradiateurs|analogInput13058|29.1211|C|4s|False|False||3|r_112_1_cio_13059_0|sondedepartchauffagedesol|analogInput13059|25.2445|C|4s|False|False||4|r_112_1_cio_13060_0|pot.physiqueconsignedepartchauffagedesol(-10;+10C)|analogInput13060|4.91234|C|4s|False|False||5|r_112_1_cio_13061_0|pot.physiqueconsignedepartradiateurs(-10;+10C)|analogInput13061|2.925|C|4s|False|False||6|r_112_1_cio_13062_0|sondeambiancebureaudirectionrez|analogInput13062|24.434|C|4s|False|False|...>>>device.points.dump('sonde')# output can be filtered (by part of names or descriptions)+----+---------------------+-----------------------------------------------------+------------------+---------+-------+-----+-------+-------+|#  | name                | description                                         | descriptor       |   value | label | age |  COV  |  OoS  |+----+---------------------+-----------------------------------------------------+------------------+---------+-------+-----+-------+-------+|0|r_112_1_cio_13056_0|sondeexterieure|analogInput13056|25.572|C|11s|False|False||1|r_112_1_cio_13057_0|sondedepartchaudiere|analogInput13057|25.0248|C|11s|False|False||2|r_112_1_cio_13058_0|sondedepartradiateurs|analogInput13058|29.1211|C|11s|False|False||3|r_112_1_cio_13059_0|sondedepartchauffagedesol|analogInput13059|25.2689|C|11s|False|False||6|r_112_1_cio_13062_0|sondeambiancebureaudirectionrez|analogInput13062|24.4437|C|11s|False|False||8|r_112_1_cio_13064_0|sondeambiancebureaudirectioncotehallrez|analogInput13064|23.9457|C|11s|False|False||10|r_112_1_cio_13066_0|sondeambiancesalledeconferences|analogInput13066|24.307|C|11s|False|False||11|r_112_1_cio_13067_0|sondeambiancetemperaturebureaucomptabiliterez|analogInput13067|23.2328|C|11s|False|False||12|r_112_1_cio_13068_0|sondeambiancebureauschematiques-sol|analogInput13068|22.5492|C|11s|False|False||14|r_112_1_cio_13070_0|sondeambiancebureauindividuels-sol|analogInput13070|23.4086|C|11s|False|False|+----+---------------------+-----------------------------------------------------+------------------+---------+-------+-----+-------+-------+Using the BACPoints.dump() method ca take some time on slow devices as this implies some read requests. The points.quickDump() provide a minimal dump avoiding some extra requests. Each point of theBACPointsobject is accessible by it’sindex(pure internal index of a point in it’s BACPoints array),typeor a part ofsomething belongingto it>>>point=points[8]>>>point<BACPointAnalogInput(r_112_1_cio_13064_0:analogInput#13064=26.51 degreesCelsius)>>>>point.dump()+--------------+-----------------------------------------------+|property|value|+--------------+-----------------------------------------------+|class|BACPointAnalogInput||name|r_112_1_cio_13064_0||description|sondeambiancebureaudirectioncotehallrez||type|analogInput||address|13064||value|26.57267189025879||state|26.57||unit|degreesCelsius(C)||COV|False||OutOfService|False||index|8|+--------------+-----------------------------------------------+>>>point=device.points.analogInput(13064)>>>point=bacnet[8015].points.analogOuput(18181)>>>points['sonde hall']# return the first object matching to this<BACPointAnalogInput(r_112_1_cio_13064_0:analogInput#13064=26.55 degreesCelsius)>>>>point=point['r_112_1_cio_13064_0']>>>point=point['13064']>>>point=point['analogInput13064']The BACPoints object offer many helpers to search/access it’s points. BACPoints is also iterable. Points are exposed throughBACPointobjects (generic class), derived in BACPointBinaryInput, BACPointBinaryOutput, BACPointAnalogInput, BACPointAnalogOutput, BACPointBinaryValue, BACPointAnalogValue,
BACPointMultiStateInput, BACPointMultiStateOutput, BACPointMultiStateValue objects, each providing specialized BACPoint extensions. You will have to dig a bit into theses objects to learn what helper they provide. Usingbpythoninteractive interpreter with it’s autocompletion feature is a very convenient way to discover thoses object (with the actual lack of documentation)>>>point.┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐│activePriorityaddressbacnetPropertybacnetpropertiescelciusToFahrenheitcov││covCanceldescriptiondigDecimalsdigUnitdigUnitStrdump││fahrenheitToCelciusindexisAnalogisBinaryisCOVisMultiState││isOutOfServiceisWritablelabelmatchnameonInit││pollpollStopprioritypropertiesreadrefresh││reloadBacnetPropertiesstatetoCelciustypeunitunitNumber││value│└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘>>>point.value26.699626922607422>>>point.unit'degreesCelsius'>>>point.value=12.0# if a point is writable, this will change it's value>>>point.write(12.0,priority=8)>>>point.write(12.0,prop='presentValue',priority=8)# for binary values>>>point.on()>>>point.off()>>>point.toggle()>>>point.isOn()>>>point.isOff()>>>point.label>>>point.labels>>>point.value=1>>>point.value='on'# for multiState values>>>point.state>>>point.label>>>point.labels>>>point.value>>>point.value=2Refreshing point’s valuesA BACDevice automatically refresh it’s points every 15s (the device’s polling time could be specified at object creation/declaration).>>>device=b.declareDevice(did=8015,address='192.168.0.15',poll=60)┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐│b.declareDevice:(self,did,address=None,poll=15,filterOutOfService=False,objectList=None)││declareDevicedeclareDeviceFromAddressdeclareDeviceFromId││declarearemotedevicespecifiedbyit's id (did, i.e 8015) and it'saddress(i.e192.168.0.15or2001:3)│└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘You can stop the polling with device.pollStop() or adjust the polling period (seconds) with device.poll(60). This is the device pollingglobalsetting. Every point may also be polled individually with point.poll(10) and point.pollStop(). Of course you may wish to set an individual poll for each point of the device with device.points.poll(60). But using theglobaldevice.poll() mechanism is a lot more efficient way to do it.Refresh may also be done throug COV (Change Of Value) mechanism. By default, COV is not enabled on a device. You can enable COV subscriptions on a secific point with point.cov(), and disable it with point.covCancel(). This can also be done on each points with device.points.cov() or with it’s shortcut device.cov(ttl=300). By default, the COV timeout is set to 300s. The poll and/or COV mechanism ensure the autorefresh of the points values. If needed, a point can be refreshed manually with point.refresh() – trigggering a read request on the presentValue. As suspected, the device.refresh() or device.points.refresh() does this globally.Going furtherIf aBACPointobject doesn’t expose something that would be useful, either ask it (we will try to add this support) or use the underlying .bac0point object which is the BAC0’sPoint objectassociated to this point.
If aBACDeviceobject doesn’t expose something that would be useful, you can use the underlying .bac0device BAC0device object.
If theBACobject doesn’t expose something that would be useful, you can use the underlying .bac0 BAC0application object.Integrated NodeThe module provide a simple BACnet browser application you can start with “python -i -m digimat.bac0 [–ip “192.168.0.84/24”] [–router x.x.x.x] [–debug]”. This will launch the following applicationparser=argparse.ArgumentParser(description='BACnet/IP browser')parser.add_argument('--router',dest='router',type=str,help='BBMD router address')parser.add_argument('--network',dest='network',type=str,help='optional ip/netsize of the BACnet/IP interface')parser.add_argument('--discover',dest='discover',action='store_true',help='launch a discover at startup')parser.add_argument('--debug',dest='debug',action='store_true',help='enable debug/verbose mode')args=parser.parse_args()bacnet=BAC(network=args.network,router=args.router)ifargs.debug:bacnet.BAC0LogDebug()ifargs.discover:bacnet.discover():bacnet.dump()When launched interactively (python -i -m digimat.bac0), you’ll have a workingbacnetvariable (a BAC object) ready to be used in just one command line.TodoWe will try to add objects and methods docstring as soon as possible to help the use of theses objects. Please let us know (fhess@st-sa.ch) is this is useful for someone (for us it is).