Skip to Content

BACnet with Raspberry PLC

Read, write and serve BACnet/IP objects from a Raspberry PLC with the current asynchronous BAC0
July 8, 2024 by
BACnet with Raspberry PLC
Boot & Work Corp. S.L, Martí Schnabel

BACnet (Building Automation and Control Networks, standard ASHRAE 135 and ISO 16484-5) is the protocol that building management systems use to read and command HVAC, lighting and energy equipment. On a Raspberry PLC you use BACnet/IP with BAC0, a Python library based on BACpypes3: install it in a virtual environment, open the connection with async with BAC0.start(ip="IP/mask"), find devices with who_is(), read with read() and command values with a write at a priority. The PLC can also publish its own values as BACnet objects, so that the building management system reads it like any other controller.

This guide explains the BACnet concepts you need, how to install the current BAC0 on a Raspberry PLC, and two scripts: a client that reads and writes another BACnet device, and a server that turns the Raspberry PLC into a BACnet device. Both scripts were tested on 27/10/2026 with BAC0 2026.7.25 and BACpypes3 0.0.110, between two BAC0 instances on a Linux PC.

  1. Step 1NetworkPLC and BACnet devices on the same IP subnet, UDP 47808
  2. Step 2Install BAC0Python 3.10 or later, in a virtual environment
  3. Step 3Read and writewho_is(), read(), readMultiple() and write at a priority
  4. Step 4Serve objectsThe PLC as a BACnet device for the BMS

BACnet in five concepts

Every BACnet device is seen the same way, whatever its manufacturer. A building management system (BMS) does not read registers, as in Modbus: it reads named objects with typed properties.

ConceptWhat it isExample
DeviceEach controller on the network. It has a device instance that must be unique in the whole BACnet network (0 to 4194302)device 1001
ObjectA point of the device, with a type and an instance numberanalogInput 1, analogValue 1, binaryOutput 3
PropertyA data field of an objectpresentValue, objectName, units, statusFlags
ServiceThe request a device sends to another oneWho-Is and I-Am to discover, ReadProperty, ReadPropertyMultiple, WriteProperty, COV subscriptions
Priority array16 levels in commandable objects. The value at the highest priority wins. Writing null releases a levelPriority 8 is manual operator. Priority 16 is the lowest

BACnet/IP and BACnet MS/TP

BACnet runs on several networks. The two usual ones in buildings are BACnet/IP, over Ethernet or Wi-Fi on UDP port 47808, and BACnet MS/TP, over RS-485. BAC0 works only with BACnet/IP. To reach MS/TP devices from a Raspberry PLC, put a BACnet router between the IP network and the MS/TP bus: the PLC then reads them with an address such as 2:5 (network 2, MAC address 5).

What you need

  • A Raspberry PLC connected by Ethernet to the network of the building, or directly to the PC for a test.
  • Another BACnet/IP device to read: a controller, a second Raspberry PLC running the server script below, or a BACnet explorer on your PC.
  • All of them on the same IP subnet, with UDP port 47808 open. Who-Is is a broadcast, and broadcasts do not cross IP routers.
  • Python 3.10 or later on the PLC. Check it with python3 --version.

Install BAC0 on the Raspberry PLC

Recent versions of Raspberry Pi OS do not let pip install packages in the system Python, so create a virtual environment and install BAC0 inside it. pip installs BACpypes3 with it.

python3 -m venv ~/bacnet
~/bacnet/bin/pip install BAC0
~/bacnet/bin/python -c "import BAC0; print(BAC0.version)"

Run your scripts with the Python of the environment: ~/bacnet/bin/python bacnet_client.py. Find the IP address and the mask of the PLC with ip a: for 192.168.1.20 with mask 255.255.255.0, write 192.168.1.20/24.

BAC0 changed in 2024. Since the 2024 versions, BAC0 is asynchronous and based on BACpypes3. Scripts written for older versions, such as bacnet = BAC0.lite(ip=...) followed by bacnet.whois() without await, do not work with the current version. The examples on this page use the current syntax. BAC0.start(), BAC0.lite() and BAC0.connect() are the same function.

Example 1: read and write a BACnet device

The script discovers the devices on the network, reads one property, reads three properties in one request and commands a binary value for 5 seconds. Change PLC_IP and DEVICE for your network.

import asyncio
import BAC0

PLC_IP = "192.168.1.20/24"   # IP address and mask of this Raspberry PLC
DEVICE = "192.168.1.50"      # IP address of the BACnet device to read

async def main():
    async with BAC0.start(ip=PLC_IP, deviceId=1002) as bacnet:
        # 1. Find the BACnet devices on the network (Who-Is / I-Am)
        devices = await bacnet.who_is()
        for iam in devices:
            print("Found", iam.iAmDeviceIdentifier, "at", iam.pduSource)

        # 2. Read one property (ReadProperty)
        value = await bacnet.read(f"{DEVICE} analogValue 1 presentValue")
        print("AV1 presentValue:", value)

        # 3. Read several properties in one request (ReadPropertyMultiple)
        print(await bacnet.readMultiple(
            f"{DEVICE} analogValue 1 objectName presentValue units"))

        # 4. Command a value at priority 8, then release it (WriteProperty)
        await bacnet._write(f"{DEVICE} binaryValue 1 presentValue active - 8")
        await asyncio.sleep(5)
        await bacnet._write(f"{DEVICE} binaryValue 1 presentValue null - 8")

asyncio.run(main())

How it works:

  • BAC0.start() opens BACnet/IP on port 47808 of the IP address you give. deviceId is the device instance of the PLC: choose one that no other device uses.
  • who_is() sends a broadcast Who-Is and returns the I-Am answers. Give it an address, who_is(address="192.168.1.50"), to ask a single device.
  • read() takes a text with the address, the object type, the instance and the property. The address can be an IP address, an IP address with a port, or network:MAC for a device behind a BACnet router.
  • readMultiple() reads several properties of an object in one ReadPropertyMultiple request: less traffic when you poll many points.
  • The write text ends with - 8: the value goes to priority 8 of the priority array. Writing null at the same priority releases it, and the object returns to the value of the next priority or to its relinquish default.

In the current BAC0, the write that you can wait for with await is _write(). write() starts the same write as a background task and does not return the result.

BACnet sends analog values as REAL, a 32-bit floating point number. A value of 22.7 can arrive as 22.700000762939453: round it before you show it or store it.

Which priority to write

PriorityUse in the standard
1Manual life safety
2Automatic life safety
5Critical equipment control
6Minimum on and off times
8Manual operator
3, 4, 7 and 9 to 16Free for the application. 16 is the lowest

Agree on the priority with the integrator of the building. A value written at a high priority and never released keeps the equipment in that state, whatever the BMS commands at a lower priority.

Example 2: the Raspberry PLC as a BACnet device

A BMS often needs to read values that the PLC measures or calculates. With BAC0, the PLC can serve its own objects: here an analog value with a room temperature and a commandable binary value that the BMS writes to start a fan.

import asyncio
import random
import BAC0
from BAC0.core.devices.local.factory import analog_value, binary_value

PLC_IP = "192.168.1.20/24"   # IP address and mask of this Raspberry PLC

def read_temperature():
    # Replace with the reading of your sensor (analog input, Modbus...)
    return round(random.uniform(20.0, 24.0), 1)

def set_fan(on):
    # Replace with the code that switches your output
    print("Fan", "ON" if on else "OFF")

async def main():
    async with BAC0.start(ip=PLC_IP, deviceId=1001,
                          localObjName="RaspberryPLC-1") as bacnet:
        analog_value(instance=1, name="Room_Temperature", presentValue=21.5,
                     properties={"units": "degreesCelsius"},
                     description="Room temperature")
        objects = binary_value(instance=1, name="Fan_Command",
                               presentValue=False, is_commandable=True)
        objects.add_objects_to_application(bacnet)

        app = bacnet.this_application.app
        temperature = app.get_object_name("Room_Temperature")
        fan = app.get_object_name("Fan_Command")

        while True:
            temperature.presentValue = read_temperature()
            set_fan(str(fan.presentValue) == "active")
            await asyncio.sleep(1)

asyncio.run(main())

How it works:

  • analog_value() and binary_value() define the objects, with their name, instance, initial value and units. is_commandable=True gives the object a priority array, so other devices can write it at a priority.
  • add_objects_to_application() publishes the objects. The BMS finds the PLC with Who-Is as device 1001 and reads its object list.
  • The loop updates the temperature every second and applies the fan command to the output. Replace read_temperature() and set_fan() with the reading of your input and the control of your output.

Troubleshooting

What you seeLikely causeWhat to do
InitializationError: IP Address provided ... invalidAnother program uses UDP port 47808, or the IP address is not on the PLCStop the other BACnet program or use another port with ip="192.168.1.20/24:47809". Check the address with ip a
who_is() returns an empty listThe devices are on another IP subnet, or a firewall blocks UDP 47808Ask the device by its address. Across subnets, BACnet/IP needs a BBMD: BAC0 registers with one through the bbmdAddress and bbmdTTL parameters
NoResponseFromControllerThe device does not answer the readCheck the address, the network number if there is a router, and the device instance
The log shows unknown-object, or the read raises UnknownPropertyErrorThe object or the property does not exist in that deviceRead the object list of the device first: read("192.168.1.50 device 1234 objectList")
The write has no effectA higher priority holds the value, or the object is not commandableRead the priorityArray property of the object and ask which level the BMS uses

BACnet or Modbus TCP in a building

Many HVAC units and meters speak Modbus, while the BMS of the building asks for BACnet. There are two common designs. The PLC serves a fixed Modbus TCP map and a gateway next to it translates the map to BACnet, as in the air handling unit case below. Or the Raspberry PLC reads the Modbus devices and publishes the values as BACnet objects itself, with the server script of this page. For Modbus on the Raspberry PLC, see the Modbus TCP and RTU guide for Raspberry PLC.

Take it further

BACnet lives in buildings. See a Raspberry PLC run the HVAC of a shopping centre, a simulator of building zones and a unit controller that talks to the BMS.

FAQ about BACnet on the Raspberry PLC

Which port does BACnet/IP use?

UDP port 47808 (0xBAC0 in hexadecimal, which gives its name to BAC0). Open it in the firewall of the PLC and of the network.

Can the Raspberry PLC talk BACnet MS/TP over RS-485?

Not with BAC0, which only supports BACnet/IP. Use a BACnet router between the IP network and the MS/TP bus, and read the MS/TP devices through it.

Why does my old BAC0 script stop working?

Since the 2024 versions, BAC0 is asynchronous. Start it with async with BAC0.start(...) inside an async function and wait for each call with await. The method whois() is now who_is().

Can a building management system read the Raspberry PLC?

Yes. With the server script of this page, the PLC answers Who-Is and serves its objects. The BMS reads them and writes the commandable ones like those of any other BACnet controller.

Is BACnet free to use?

Yes. BACnet is an open standard of ASHRAE, also published as ISO 16484-5. You can use it without a licence. BAC0 is open source, under the LGPLv3 licence.

​Search in our Blog

BACnet with Raspberry PLC
Boot & Work Corp. S.L, Martí Schnabel July 8, 2024
Share this post
Tags

Looking for your ideal Programmable Logic Controller?

Take a look at this product comparison with other industrial controllers Arduino-based. 

We are comparing inputs, outputs, communications and other features with the ones of the relevant brands.

PLC Comparison