Getting Started¶
Installation¶
Install bac-py from PyPI:
pip install bac-py
To enable JSON serialization support (using orjson):
pip install bac-py[serialization]
To enable BACnet Secure Connect (BACnet/SC) support:
pip install bac-py[secure]
Requirements¶
Python >= 3.13
No runtime dependencies for the core library
Optional:
orjson(installed with theserializationextra)Optional:
websocketsandcryptography(installed with thesecureextra for BACnet/SC support)
Development Setup¶
git clone https://github.com/jscott3201/bac-py.git
cd bac-py
uv sync --group dev
Your First Read¶
The simplest way to read a value from a BACnet device:
import asyncio
from bac_py import Client
async def main():
async with Client(instance_number=999) as client:
value = await client.read("192.168.1.100", "ai,1", "pv")
print(f"Temperature: {value}")
asyncio.run(main())
The Client class is an async context manager that
handles starting and stopping the underlying BACnet application. It binds a
UDP socket, assigns your device a BACnet instance number, and is ready to
communicate.
See Reading Properties for more read examples, or Client Guide for the full client capabilities reference.
Your First Write¶
Writing a value is equally straightforward. bac-py automatically encodes Python values to the correct BACnet application tag:
import asyncio
from bac_py import Client
async def main():
async with Client(instance_number=999) as client:
await client.write("192.168.1.100", "av,1", "pv", 72.5, priority=8)
print("Write complete.")
asyncio.run(main())
The priority parameter sets the BACnet command priority (1–16). Priority 8
is commonly used for manual operator commands.
See Writing Properties for more write examples including the full encoding rules table.
String Aliases¶
The convenience API accepts short aliases for common object types and property
identifiers so you don’t need to type out full names. Full hyphenated names
("analog-input,1", "present-value") and enum values
(ObjectType.ANALOG_INPUT, PropertyIdentifier.PRESENT_VALUE) are always
accepted as well.
Object type aliases:
Alias |
Object Type |
Alias |
Object Type |
|---|---|---|---|
|
analog-input |
|
lighting-output |
|
analog-output |
|
binary-lighting-output |
|
analog-value |
|
load-control |
|
large-analog-value |
|
accumulator |
|
binary-input |
|
pulse-converter |
|
binary-output |
|
timer |
|
binary-value |
|
event-enrollment |
|
multi-state-input |
|
alert-enrollment |
|
multi-state-output |
|
notification-forwarder |
|
multi-state-value |
|
averaging |
|
device |
|
integer-value |
|
file |
|
positive-integer-value |
|
notification-class |
|
characterstring-value |
|
network-port |
|
bitstring-value |
|
calendar |
|
octetstring-value |
|
command |
|
date-value |
|
channel |
|
datetime-value |
|
program |
|
time-value |
|
schedule |
|
structured-view |
|
trend-log |
|
group |
|
trend-log-multiple |
|
global-group |
|
event-log |
|
life-safety-point |
|
loop |
|
life-safety-zone |
|
access-door |
||
|
access-point |
||
|
audit-reporter |
||
|
audit-log |
Property identifier aliases:
Alias |
Property |
Alias |
Property |
|---|---|---|---|
|
present-value |
|
polarity |
|
object-name |
|
active-text |
|
object-type |
|
inactive-text |
|
description |
|
number-of-states |
|
units |
|
state-text |
|
status-flags |
|
event-enable |
|
out-of-service |
|
acked-transitions |
|
reliability |
|
notify-type |
|
event-state |
|
time-delay |
|
object-list |
|
notification-class |
|
property-list |
|
limit-enable |
|
profile-name |
|
log-buffer |
|
priority-array |
|
record-count |
|
relinquish-default |
|
log-enable |
|
min-pres-value |
|
weekly-schedule |
|
max-pres-value |
|
exception-schedule |
|
resolution |
|
schedule-default |
|
cov-increment |
|
system-status |
|
deadband |
|
vendor-name |
|
high-limit |
|
vendor-identifier |
|
low-limit |
|
model-name |
|
firmware-revision |
||
|
application-software-version |
||
|
max-apdu-length-accepted |
||
|
segmentation-supported |
||
|
database-revision |
||
|
protocol-version |
||
|
protocol-revision |
Addressing¶
The convenience API accepts device addresses as plain strings:
# IP only (default BACnet port 47808)
await client.read("192.168.1.100", "ai,1", "pv")
# IP with explicit port
await client.read("192.168.1.100:47808", "ai,1", "pv")
# Routed address (network:ip:port)
await client.read("5:192.168.1.100:47808", "ai,1", "pv")
# Ethernet MAC address (colon-separated hex)
addr = parse_address("aa:bb:cc:dd:ee:ff")
# Remote Ethernet address on network 5
addr = parse_address("5:aa:bb:cc:dd:ee:ff")
# Remote MS/TP or non-IP station (network:hex_mac)
addr = parse_address("4352:01") # 1-byte MS/TP address on network 4352
addr = parse_address("100:0a0b") # 2-byte address on network 100
Configuration¶
DeviceConfig controls device identity and
network parameters:
from bac_py import DeviceConfig
config = DeviceConfig(
instance_number=999, # Device instance (0-4194302)
name="bac-py", # Device name
vendor_name="bac-py", # Vendor name
vendor_id=0, # ASHRAE vendor ID
interface="0.0.0.0", # IP address to bind
port=0xBAC0, # UDP port (47808)
max_apdu_length=1476, # Max APDU size
apdu_timeout=6000, # Request timeout (ms)
apdu_retries=3, # Retry count
max_segments=None, # Max segments (None = unlimited)
)
async with Client(config) as client:
value = await client.read("192.168.1.100", "ai,1", "pv")
Transport selection – DeviceConfig also supports IPv6, BACnet/SC, and
Ethernet transports (mutually exclusive):
# IPv6 transport (Annex U)
config = DeviceConfig(instance_number=999, ipv6=True)
# BACnet/SC transport (Annex AB) -- requires bac-py[secure]
from bac_py.transport.sc import SCTransportConfig
from bac_py.transport.sc.tls import SCTLSConfig
config = DeviceConfig(
instance_number=999,
sc_config=SCTransportConfig(
primary_hub_uri="wss://hub.example.com:8443",
tls_config=SCTLSConfig(...),
),
)
# Ethernet transport (Clause 7) -- requires root/CAP_NET_RAW
config = DeviceConfig(instance_number=999, ethernet_interface="eth0")
See Transport Setup for full transport configuration details.
For simple client use cases, you can skip DeviceConfig and pass common
options directly:
async with Client(instance_number=999, interface="192.168.1.50") as client:
...
# IPv6 and SC transports also work with Client directly
async with Client(instance_number=999, ipv6=True) as client:
...
Error Handling¶
All client methods raise from a common exception hierarchy:
from bac_py.services.errors import (
BACnetBaseError, # Base for all BACnet errors
BACnetError, # Error-PDU (error_class, error_code)
BACnetRejectError, # Reject-PDU (reason)
BACnetAbortError, # Abort-PDU (reason)
BACnetTimeoutError, # No response after all retries
)
Example:
from bac_py.services.errors import BACnetError, BACnetTimeoutError
try:
value = await client.read("192.168.1.100", "ai,1", "pv")
except BACnetTimeoutError:
print("Device did not respond")
except BACnetError as e:
print(f"BACnet error: class={e.error_class}, code={e.error_code}")
Debugging and Logging¶
bac-py includes structured logging throughout the stack using Python’s standard
logging module. Enable it to see what’s happening under the hood:
import logging
# Show lifecycle events (start, stop, subscriptions, etc.)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(name)s %(levelname)s %(message)s",
)
# Or for detailed protocol traces
logging.basicConfig(level=logging.DEBUG)
You can target specific modules to reduce noise:
# Only debug client operations
logging.getLogger("bac_py.app.client").setLevel(logging.DEBUG)
See Debugging and Logging for the full logger hierarchy reference, practical debugging recipes, and file logging configuration.
Two API Levels¶
bac-py exposes two API levels. Use whichever fits your needs:
Client – simplified wrapper for common client tasks.
Accepts string addresses, string object/property identifiers, and Python
values. Ideal for scripts, integrations, and most client-side work.
BACnetApplication +
BACnetClient – full protocol-level access. Use
this when you need server handlers, router mode, custom service registration,
raw encoded bytes, or direct access to the transport and network layers. See
Server Mode for server examples and Protocol-Level API for
client-side protocol-level usage.
The Client wrapper exposes both levels. All BACnetClient protocol-level
methods are available alongside the convenience methods, and the underlying
BACnetApplication is accessible via client.app.