Asynchronous BACnet protocol library for Python — BACnet/IP, IPv6, Ethernet, and Secure Connect
This project has been archived.
The maintainers of this project have marked this project as archived. No new releases are expected.
Project description
bac-py
Asynchronous BACnet/IP protocol library for Python 3.13+, implementing ASHRAE Standard 135-2020. 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")
Table of Contents
- Features
- Installation
- Quick Start
- API Levels
- Configuration
- Architecture
- Examples
- Testing
- Requirements
- License
Features
| 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 | 40+ 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,380+ unit tests, Docker integration tests, local benchmarks, type-safe enums and frozen dataclasses throughout |
Installation
pip install bac-py
Optional 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] # Both
Development
git clone https://github.com/jscott3201/bac-py.git
cd bac-py
uv sync --group dev
Quick Start
Read a Property
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 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.
Write a Value
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) # Relinquish
Values 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) |
Read Multiple Properties
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}")
Discover Devices
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}")
Subscribe to COV
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,
)
Serve Objects
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.
API Levels
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.
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 Example
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:
address = parse_address("192.168.1.100")
obj_id = ObjectIdentifier(ObjectType.ANALOG_VALUE, 1)
await client.write_property(
address, obj_id,
PropertyIdentifier.PRESENT_VALUE,
value=encode_application_real(72.5),
priority=8,
)
Configuration
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)
)
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,
),
)
Architecture
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/ 40+ 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
Key Classes
| 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 |
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, # Timeout after all retries
)
Examples
The examples/ directory contains 23 runnable scripts. 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 |
secure_connect.py |
Connect to a BACnet/SC hub and exchange NPDUs |
secure_connect_hub.py |
Run a BACnet/SC hub with object serving |
ip_to_sc_router.py |
Bridge BACnet/IP and BACnet/SC networks |
ipv6_client_server.py |
BACnet/IPv6 client and server with foreign device |
interactive_cli.py |
Menu-driven interactive CLI for exploring the full API |
sc_generate_certs.py |
Generate test PKI and demonstrate TLS-secured SC |
Testing
make test # 6,380+ 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 issues
Local Benchmarks
Single-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 integration
Docker Integration Tests
Real 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-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
Requirements
- 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
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for development setup, code standards, and the pull request process.
For security vulnerabilities, see SECURITY.md.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bac_py-1.5.0.tar.gz.
File metadata
- Download URL: bac_py-1.5.0.tar.gz
- Upload date:
- Size: 726.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c82a03352ed5d706115e9d1b05415d581158db9d3fdec4a54e7d4291f4bea187
|
|
| MD5 |
dba5ad01b1a3fe36b424a9b028a881ec
|
|
| BLAKE2b-256 |
f12bef2fcae380a8c12dae0d7cb93e19696b18702938d83b204284a543ba4718
|
Provenance
The following attestation bundles were made for bac_py-1.5.0.tar.gz:
Publisher:
release.yml on jscott3201/bac-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bac_py-1.5.0.tar.gz -
Subject digest:
c82a03352ed5d706115e9d1b05415d581158db9d3fdec4a54e7d4291f4bea187 - Sigstore transparency entry: 953566283
- Sigstore integration time:
-
Permalink:
jscott3201/bac-py@26130e365e45226f2dbccbf87bbb5078aea52782 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/jscott3201
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@26130e365e45226f2dbccbf87bbb5078aea52782 -
Trigger Event:
workflow_run
-
Statement type:
File details
Details for the file bac_py-1.5.0-py3-none-any.whl.
File metadata
- Download URL: bac_py-1.5.0-py3-none-any.whl
- Upload date:
- Size: 375.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4172506c77e9e4190ddfc392bee03e8135ec1d372132bca3966ad0aada26511e
|
|
| MD5 |
3d2cac4120adc0fd935f7afeffa29d80
|
|
| BLAKE2b-256 |
b3714efdbee6f84c640ea1627c12114be68ab774f7b1058bbcbd9b7ce2d2b5df
|
Provenance
The following attestation bundles were made for bac_py-1.5.0-py3-none-any.whl:
Publisher:
release.yml on jscott3201/bac-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bac_py-1.5.0-py3-none-any.whl -
Subject digest:
4172506c77e9e4190ddfc392bee03e8135ec1d372132bca3966ad0aada26511e - Sigstore transparency entry: 953566285
- Sigstore integration time:
-
Permalink:
jscott3201/bac-py@26130e365e45226f2dbccbf87bbb5078aea52782 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/jscott3201
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@26130e365e45226f2dbccbf87bbb5078aea52782 -
Trigger Event:
workflow_run
-
Statement type: