Asynchronous BACnet protocol library for Python 3.13+, implementing ASHRAE Standard 135-2020 with four transports: BACnet/IP, BACnet/IPv6, BACnet Secure Connect, and BACnet Ethernet. Zero required runtime dependencies, built on native asyncio.
Documentation | Getting Started | API Reference | Changelog
from bac_py import Client
async with Client(instance_number=999) as client:
value = await client.read("192.168.1.100", "ai,1", "pv")- Features
- Installation
- Quick Start
- Transports
- API Levels
- Configuration
- Architecture
- Examples
- Testing
- Requirements
- License
| Category | Highlights |
|---|---|
| Transports | BACnet/IP (Annex J), BACnet/IPv6 with BBMD and foreign device (Annex U), BACnet Ethernet (Clause 7), BACnet Secure Connect over WebSocket/TLS 1.3 (Annex AB) |
| Client & Server | Full-duplex -- serve objects and issue requests from the same application |
| Object Model | 62 object types with property definitions, priority arrays, and commandable outputs |
| Services | All confirmed and unconfirmed services including COV, alarms, file access, audit logging, and private transfer |
| Event Reporting | All 18 event algorithms, intrinsic reporting, NotificationClass routing with day/time filtering |
| Engines | Schedule evaluation, trend logging (polled/COV/triggered), and audit record generation |
| Networking | Multi-port routing, BBMD, foreign device registration, segmented transfers, device info caching |
| Convenience API | String-based addressing ("ai,1", "pv"), smart type coercion, auto-discovery |
| Serialization | to_dict()/from_dict() on all data types; optional orjson backend |
| Conformance | BIBB declarations and PICS generation per Clause 24 |
| Quality | 6,500+ unit tests, Docker integration tests, local benchmarks, type-safe enums and frozen dataclasses throughout |
pip install bac-pyOptional extras:
pip install bac-py[serialization] # orjson for JSON serialization
pip install bac-py[secure] # WebSocket + TLS for BACnet Secure Connect
pip install bac-py[serialization,secure] # Bothgit clone https://github.com/jscott3201/bac-py.git
cd bac-py
uv sync --group devimport 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 convenience API accepts 48 object type aliases (ai, ao, av, bi,
bo, bv, msv, dev, sched, tl, nc, etc.) and 45 property
abbreviations (pv, name, type, list, status, priority, min,
max, etc.). Full names like "analog-input,1" and "present-value" also
work. See the alias reference for the complete table.
async with Client(instance_number=999) as client:
await client.write("192.168.1.100", "av,1", "pv", 72.5, priority=8)
await client.write("192.168.1.100", "bo,1", "pv", 1, priority=8)
await client.write("192.168.1.100", "av,1", "pv", None, priority=8) # RelinquishValues are automatically encoded to the correct BACnet application tag based on the Python type, target object type, and property:
| Python type | BACnet encoding |
|---|---|
float |
Real |
int (analog PV) |
Real |
int (binary PV) |
Enumerated |
int (multi-state PV) |
Unsigned |
str |
Character String |
bool |
Enumerated (1/0) |
None |
Null |
IntEnum |
Enumerated |
bytes |
Pass-through (pre-encoded) |
async with Client(instance_number=999) as client:
results = await client.read_multiple("192.168.1.100", {
"ai,1": ["pv", "object-name", "units"],
"ai,2": ["pv", "object-name"],
"av,1": ["pv", "priority-array"],
})
for obj_id, props in results.items():
print(f"{obj_id}:")
for name, value in props.items():
print(f" {name}: {value}")from bac_py import Client
async with Client(instance_number=999) as client:
devices = await client.discover(timeout=3.0)
for dev in devices:
print(f" {dev.instance} {dev.address_str} vendor={dev.vendor_id}")from bac_py import Client, decode_cov_values
async with Client(instance_number=999) as client:
def on_notification(notification, source):
values = decode_cov_values(notification)
for name, value in values.items():
print(f" {name}: {value}")
await client.subscribe_cov_ex(
"192.168.1.100", "ai,1",
process_id=1,
callback=on_notification,
lifetime=3600,
)from bac_py import BACnetApplication, DefaultServerHandlers, DeviceConfig, DeviceObject
from bac_py.objects.analog import AnalogInputObject
from bac_py.types.enums import EngineeringUnits
async def serve():
config = DeviceConfig(
instance_number=100,
name="My-Device",
vendor_name="ACME",
vendor_id=999,
)
async with BACnetApplication(config) as app:
device = DeviceObject(
instance_number=100,
object_name="My-Device",
vendor_name="ACME",
vendor_identifier=999,
)
app.object_db.add(device)
app.object_db.add(AnalogInputObject(
instance_number=1,
object_name="Temperature",
units=EngineeringUnits.DEGREES_CELSIUS,
present_value=22.5,
))
handlers = DefaultServerHandlers(app, app.object_db, device)
handlers.register()
await app.run()The server automatically handles ReadProperty, WriteProperty, ReadPropertyMultiple, WritePropertyMultiple, ReadRange, Who-Is, COV subscriptions, device management, file access, and object management.
bac-py supports four BACnet transports. The transport is selected via
Client(...) or DeviceConfig(...) parameters -- all BACnet services work
identically regardless of transport.
Standard UDP transport on port 47808. No extra dependencies.
async with Client(instance_number=999) as client:
value = await client.read("192.168.1.100", "ai,1", "pv")IPv6 transport with multicast discovery (Annex U). No extra dependencies.
async with Client(instance_number=999, ipv6=True) as client:
devices = await client.discover(timeout=3.0)TLS 1.3 WebSocket hub-and-spoke topology (Annex AB). Requires pip install bac-py[secure].
from bac_py.transport.sc import SCTransportConfig
from bac_py.transport.sc.tls import SCTLSConfig
sc_config = SCTransportConfig(
primary_hub_uri="wss://hub.example.com:8443",
tls_config=SCTLSConfig(
ca_certificates_path="ca.pem",
certificate_path="device.pem",
private_key_path="device.key",
),
)
async with Client(instance_number=999, sc_config=sc_config) as client:
devices = await client.discover(timeout=5.0)Raw IEEE 802.3/802.2 LLC frames (Clause 7). Requires root/CAP_NET_RAW on Linux or BPF access on macOS. No extra dependencies.
async with Client(instance_number=999, ethernet_interface="eth0") as client:
value = await client.read("01:02:03:04:05:06", "ai,1", "pv")The same transport options work for servers via DeviceConfig:
# IPv6 server
config = DeviceConfig(instance_number=100, ipv6=True)
# BACnet/SC server (hub + node)
from bac_py.transport.sc import SCTransportConfig
from bac_py.transport.sc.hub_function import SCHubConfig
from bac_py.transport.sc.tls import SCTLSConfig
config = DeviceConfig(
instance_number=100,
sc_config=SCTransportConfig(
hub_function_config=SCHubConfig(
bind_address="0.0.0.0", bind_port=8443, tls_config=tls,
),
tls_config=tls,
),
)
# Ethernet server
config = DeviceConfig(instance_number=100, ethernet_interface="eth0")See the Transport Setup Guide and Server Mode Guide for full details.
bac-py offers two API levels:
Client -- simplified wrapper for common tasks. Accepts string addresses,
string object/property identifiers, and Python values. Ideal for scripts,
integrations, and most client-side work. Both convenience methods (read,
write) and protocol-level methods (read_property, write_property, etc.)
accept flexible string inputs.
BACnetApplication + BACnetClient -- full protocol-level access for
server handlers, router mode, custom service registration, raw encoded bytes,
and direct transport/network layer access.
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.
Protocol-level methods accept the same flexible string inputs as convenience methods, while working with raw encoded bytes:
from bac_py.encoding.primitives import encode_application_real
async with Client(instance_number=999) as client:
await client.write_property(
"192.168.1.100", "av,1", "pv",
value=encode_application_real(72.5),
priority=8,
)Typed objects also work (and are passed through with zero overhead):
from bac_py.encoding.primitives import encode_application_real
from bac_py.network.address import parse_address
from bac_py.types.enums import ObjectType, PropertyIdentifier
from bac_py.types.primitives import ObjectIdentifier
async with Client(instance_number=999) as client:
await client.write_property(
parse_address("192.168.1.100"),
ObjectIdentifier(ObjectType.ANALOG_VALUE, 1),
PropertyIdentifier.PRESENT_VALUE,
value=encode_application_real(72.5),
priority=8,
)from bac_py.app.application 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)
# Transport selection (mutually exclusive):
# ipv6=True, # BACnet/IPv6 (Annex U)
# sc_config=SCTransportConfig(...), # BACnet Secure Connect (Annex AB)
# ethernet_interface="eth0", # BACnet Ethernet (Clause 7)
)For multi-network routing, add a RouterConfig:
from bac_py.app.application import DeviceConfig, RouterConfig, RouterPortConfig
config = DeviceConfig(
instance_number=999,
router_config=RouterConfig(
ports=[
RouterPortConfig(port_id=0, network_number=1,
interface="192.168.1.10", port=47808),
RouterPortConfig(port_id=1, network_number=2,
interface="10.0.0.10", port=47808),
],
application_port_id=0,
),
)src/bac_py/
app/ Application orchestration, client API, server handlers,
event engine, schedule engine, trend log engine, audit manager
encoding/ ASN.1/BER tag-length-value encoding and APDU codec
network/ Addressing, NPDU network layer, multi-port router
objects/ 62 BACnet object types with property definitions
segmentation/ Segmented message assembly and transmission
serialization/ JSON serialization (optional orjson backend)
services/ Service request/response types and handler registry
transport/ BACnet/IP, BACnet/IPv6, Ethernet 802.3, BACnet Secure Connect
types/ Primitive types, enumerations, constructed types
conformance/ BIBB declarations and PICS generation
| Class | Module | Purpose |
|---|---|---|
Client |
client |
Simplified async context manager for client use |
BACnetApplication |
app.application |
Central orchestrator -- lifecycle, APDU dispatch, engines |
BACnetClient |
app.client |
Full async API for all BACnet services |
DefaultServerHandlers |
app.server |
Standard service handlers for a server device |
DeviceObject |
objects.device |
Required device object (Clause 12.11) |
ObjectDatabase |
objects.base |
Runtime registry of local BACnet objects |
BACnetAddress |
network.address |
Network + MAC address for device targeting |
ObjectIdentifier |
types.primitives |
Object type + instance number |
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, # Timeout after all retries
)The examples/ directory contains 26 runnable scripts covering
client operations, server setup across all transports, and advanced features.
See the Examples Guide
for detailed walkthroughs.
| File | Description |
|---|---|
read_value.py |
Read properties with short aliases |
write_value.py |
Write values with auto-encoding and priority |
read_multiple.py |
Read multiple properties from multiple objects |
write_multiple.py |
Write multiple properties in a single request |
discover_devices.py |
Discover devices with Who-Is broadcast |
extended_discovery.py |
Extended discovery with profile metadata |
advanced_discovery.py |
Who-Has, unconfigured devices, hierarchy traversal |
monitor_cov.py |
Subscribe to COV and decode notifications |
cov_property.py |
Property-level COV subscriptions with increment |
alarm_management.py |
Alarm/enrollment summary, event info, acknowledgment |
text_message.py |
Send confirmed/unconfirmed text messages |
backup_restore.py |
Backup and restore device configuration |
object_management.py |
Create, list, and delete objects |
device_control.py |
Communication control, reinitialization, time sync |
audit_log.py |
Query audit log records with pagination |
router_discovery.py |
Discover routers and remote networks |
foreign_device.py |
Register as foreign device via BBMD |
ipv6_client.py |
BACnet/IPv6 client with multicast discovery |
ipv6_server.py |
BACnet/IPv6 server with BACnetApplication |
ethernet_server.py |
BACnet Ethernet server with BACnetApplication |
sc_server.py |
BACnet/SC server (hub + full APDU dispatch) |
secure_connect.py |
Low-level SC hub connection and NPDU exchange |
secure_connect_hub.py |
Low-level SC hub with manual message relay |
sc_generate_certs.py |
Generate test PKI and demonstrate TLS-secured SC |
ip_to_sc_router.py |
Bridge BACnet/IP and BACnet/SC networks |
interactive_cli.py |
Menu-driven interactive CLI for exploring the full API |
make test # 6,500+ unit tests
make lint # ruff check + format verification
make typecheck # mypy
make docs # sphinx-build
make check # all of the above
make coverage # tests with coverage report
make fix # auto-fix lint/format issuesSingle-process benchmarks for all transport types (no Docker required):
make bench-bip # BACnet/IP stress test on localhost
make bench-router # Two-network router stress test
make bench-bbmd # BBMD + foreign device stress test
make bench-sc # BACnet/SC hub + node stress test
make bench-bip-json # JSON output for CI integrationReal BACnet communication over UDP and WebSocket between containers:
make docker-build # Build image (Alpine + uv + orjson)
make docker-test # All integration scenarios
make docker-test-client # Client/server: read, write, discover, RPM, WPM
make docker-test-bbmd # BBMD: foreign device registration + forwarding
make docker-test-router # Router: cross-network discovery and reads
make docker-test-stress # BIP stress: sustained throughput (60s)
make docker-test-sc # Secure Connect: hub, node, NPDU relay
make docker-test-sc-stress # SC stress: WebSocket throughput (60s)
make docker-test-router-stress # Router stress: cross-network routing (60s)
make docker-test-bbmd-stress # BBMD stress: foreign device throughput (60s)
make docker-test-device-mgmt # Device management: DCC, time sync, text message
make docker-test-cov-advanced # COV: concurrent subscriptions, property-level COV
make docker-test-events # Events: alarm reporting, acknowledgment, queries
make docker-test-ipv6 # IPv6: BACnet/IPv6 client/server (Annex U)
make docker-test-mixed-bip-ipv6 # Mixed BIP↔IPv6: cross-transport routing
make docker-test-mixed-bip-sc # Mixed BIP↔SC: cross-transport routing (TLS)
make docker-stress # BIP stress runner (JSON report to stdout)
make docker-sc-stress # SC stress runner (JSON report to stdout)
make docker-router-stress # Router stress runner (JSON report to stdout)
make docker-bbmd-stress # BBMD stress runner (JSON report to stdout)
make docker-clean # Cleanup- Python >= 3.13
- No runtime dependencies for BACnet/IP, BACnet/IPv6, and BACnet Ethernet
- Optional:
orjsonfor JSON serialization (pip install bac-py[serialization]) - Optional:
websockets+cryptographyfor BACnet Secure Connect (pip install bac-py[secure]) - Docker and Docker Compose for integration tests
Contributions are welcome! Please see CONTRIBUTING.md for development setup, code standards, and the pull request process.
For security vulnerabilities, see SECURITY.md.
MIT