Skip to main content

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

PyPI Python License CI

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")

Table of Contents

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 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

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.

Transports

bac-py supports four BACnet transports. The transport is selected via Client(...) or DeviceConfig(...) parameters -- all BACnet services work identically regardless of transport.

BACnet/IP (default)

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")

BACnet/IPv6

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)

BACnet Secure Connect

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)

BACnet Ethernet

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")

Server Transport Selection

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.

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. 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 Example

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,
    )

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)
    # 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,
    ),
)

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/        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

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 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

Testing

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 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-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

Requirements

  • Python >= 3.13
  • No runtime dependencies for BACnet/IP, BACnet/IPv6, and BACnet Ethernet
  • Optional: orjson for JSON serialization (pip install bac-py[serialization])
  • Optional: websockets + cryptography for 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

bac_py-1.5.5.tar.gz (749.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bac_py-1.5.5-py3-none-any.whl (384.9 kB view details)

Uploaded Python 3

File details

Details for the file bac_py-1.5.5.tar.gz.

File metadata

  • Download URL: bac_py-1.5.5.tar.gz
  • Upload date:
  • Size: 749.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for bac_py-1.5.5.tar.gz
Algorithm Hash digest
SHA256 e23fd8a8252fb37f0e38966838951d678813a7ed3e86549bee0e466bb9c4a5b7
MD5 5fa5e7ff4f2b8c63784d9f1d2a5ac352
BLAKE2b-256 30c0e069524fe74b0d971b14e07e44c3266613875877d38b47402b456d962b73

See more details on using hashes here.

Provenance

The following attestation bundles were made for bac_py-1.5.5.tar.gz:

Publisher: release.yml on jscott3201/bac-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file bac_py-1.5.5-py3-none-any.whl.

File metadata

  • Download URL: bac_py-1.5.5-py3-none-any.whl
  • Upload date:
  • Size: 384.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for bac_py-1.5.5-py3-none-any.whl
Algorithm Hash digest
SHA256 d72162707dcf8664cf3669845475ce248989d2807fc42bfdda7f836f6b8189d3
MD5 4fbcbca6314c80b6b9a5860ad1605c24
BLAKE2b-256 4ddc258135cd4f6411b5fe8356ee0849ee9775c62a786f250143229623b87775

See more details on using hashes here.

Provenance

The following attestation bundles were made for bac_py-1.5.5-py3-none-any.whl:

Publisher: release.yml on jscott3201/bac-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page