Client Guide

bac-py provides two API levels for client operations. This page is the consolidated reference for all client capabilities, including features documented elsewhere and those unique to this page.

Choosing an API Level

Client

BACnetClient

String addresses ("1.2.3.4")

BACnetAddress objects

String identifiers ("ai,1", "pv")

ObjectIdentifier, PropertyIdentifier enums

Auto-encodes Python values

Pre-encoded bytes for write values

Async context manager

Requires a running BACnetApplication

Best for scripts, integrations

Best for servers, routers, custom protocol work

The Client wrapper exposes both levels. All BACnetClient methods are available alongside the convenience methods, and the underlying application is accessible via client.app.

# Convenience API
async with Client(instance_number=999) as client:
    value = await client.read("192.168.1.100", "ai,1", "pv")

# Protocol-level API (same Client object)
async with Client(instance_number=999) as client:
    from bac_py.network.address import parse_address
    from bac_py.types.enums import PropertyIdentifier, ObjectType
    from bac_py.types.primitives import ObjectIdentifier

    addr = parse_address("192.168.1.100")
    oid = ObjectIdentifier(ObjectType.ANALOG_INPUT, 1)
    ack = await client.read_property(addr, oid, PropertyIdentifier.PRESENT_VALUE)

Transport Options

Client supports all bac-py transports via constructor parameters. These are mutually exclusive – at most one transport selector should be set:

# BACnet/IP (default)
async with Client(instance_number=999) as client: ...

# BACnet/IPv6 (Annex U)
async with Client(instance_number=999, ipv6=True) as client: ...

# BACnet/SC (Annex AB) -- requires 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(...),
)
async with Client(instance_number=999, sc_config=sc_config) as client: ...

# BACnet Ethernet (Clause 7) -- requires root/CAP_NET_RAW
async with Client(
    instance_number=999,
    ethernet_interface="eth0",
) as client: ...

See Transport Setup for detailed transport configuration.

Capabilities at a Glance

Capability

Method(s)

Guide

Read properties

read(), read_multiple()

Reading Properties

Write properties

write(), write_multiple()

Writing Properties

Device discovery

who_is(), discover(), discover_extended()

Device Discovery

Object search

who_has()

Device Discovery

COV subscriptions

subscribe_cov(), subscribe_cov_property()

COV Subscriptions

Alarm management

get_alarm_summary(), acknowledge_alarm()

Alarm Management

Event information

get_event_information(), get_enrollment_summary()

Event Notifications

Text messaging

send_text_message()

Text Messaging

Object management

create_object(), delete_object()

Object Management

Backup / restore

backup(), restore()

Backup and Restore

Device control

device_communication_control(), reinitialize_device()

Device Communication Control

Time sync

time_synchronization(), utc_time_synchronization()

Time Synchronization

Audit log queries

query_audit_log()

Audit Logging

Foreign device

register_as_foreign_device()

Foreign Device Registration

Router discovery

who_is_router_to_network()

Router Discovery

File access

atomic_read_file(), atomic_write_file()

File Access (below)

Private transfer

confirmed_private_transfer(), unconfirmed_private_transfer()

Private Transfer (below)

WriteGroup

write_group()

WriteGroup (below)

Virtual terminal

vt_open(), vt_data(), vt_close()

Virtual Terminal Sessions (below)

List elements

add_list_element(), remove_list_element()

List Element Operations (below)

Hierarchy traversal

traverse_hierarchy()

Hierarchy Traversal (below)

Unconfigured devices

discover_unconfigured()

Device Discovery

File Access

Read and write BACnet File objects using the AtomicReadFile and AtomicWriteFile services (Clause 14). Two access methods are supported: stream (byte-oriented) and record (record-oriented).

Stream access

from bac_py.services.file_access import StreamReadAccess, StreamWriteAccess

# Read 1024 bytes starting at position 0
ack = await client.atomic_read_file(
    "192.168.1.100",
    "file,1",
    StreamReadAccess(file_start_position=0, requested_octet_count=1024),
)
data = ack.file_data          # bytes
eof = ack.end_of_file         # True if no more data
start = ack.file_start_position

# Read the entire file in chunks
position = 0
contents = bytearray()
while True:
    ack = await client.atomic_read_file(
        "192.168.1.100", "file,1",
        StreamReadAccess(file_start_position=position, requested_octet_count=4096),
    )
    contents.extend(ack.file_data)
    if ack.end_of_file:
        break
    position += len(ack.file_data)

# Write data at position 0
await client.atomic_write_file(
    "192.168.1.100",
    "file,1",
    StreamWriteAccess(file_start_position=0, file_data=b"Hello BACnet"),
)

Record access

from bac_py.services.file_access import RecordReadAccess, RecordWriteAccess

# Read 10 records starting at record 0
ack = await client.atomic_read_file(
    "192.168.1.100",
    "file,1",
    RecordReadAccess(file_start_record=0, requested_record_count=10),
)
records = ack.record_data     # list[bytes]
eof = ack.end_of_file

# Write records starting at record 0
await client.atomic_write_file(
    "192.168.1.100",
    "file,1",
    RecordWriteAccess(file_start_record=0, record_data=[b"rec1", b"rec2"]),
)

Private Transfer

Vendor-specific services use ConfirmedPrivateTransfer (Clause 16.2) and UnconfirmedPrivateTransfer (Clause 16.3) to exchange proprietary data.

# Confirmed (request/response)
ack = await client.confirmed_private_transfer(
    "192.168.1.100",
    vendor_id=999,
    service_number=1,
    service_parameters=b"\x01\x02\x03",  # vendor-defined encoding
)
result = ack.result_block  # vendor-defined response bytes

# Unconfirmed (fire-and-forget)
client.unconfirmed_private_transfer(
    "192.168.1.100",
    vendor_id=999,
    service_number=2,
    service_parameters=b"\x04\x05",
)

The service_parameters and response result_block are opaque bytes whose encoding is defined by the vendor. Both the vendor_id (ASHRAE- assigned) and service_number (vendor-defined) identify the specific operation.

WriteGroup

WriteGroup (Clause 15.11) is an unconfirmed service for writing values to multiple Channel objects via group addressing. It is commonly used in lighting and HVAC control for coordinated group commands.

from bac_py.services.write_group import GroupChannelValue
from bac_py.encoding.primitives import encode_application_real

# Write to channels 1 and 2 at priority 8
client.write_group(
    "192.168.1.255",  # broadcast to subnet
    group_number=1,
    write_priority=8,
    change_list=[
        GroupChannelValue(
            channel=1,
            value=encode_application_real(75.0),
        ),
        GroupChannelValue(
            channel=2,
            value=encode_application_real(50.0),
            overriding_priority=1,  # optional per-channel priority override
        ),
    ],
)

Each GroupChannelValue targets a channel number with application-tagged encoded value bytes. The overriding_priority optionally overrides the request-level write_priority for that specific channel.

WriteGroup is fire-and-forget (unconfirmed) and is typically broadcast.

Virtual Terminal Sessions

The Virtual Terminal (VT) services (Clause 17) provide a character-based terminal interface to BACnet devices, useful for device diagnostics and configuration.

from bac_py.types.enums import VTClass

# Open a session
ack = await client.vt_open(
    "192.168.1.100",
    vt_class=VTClass.DEFAULT_TERMINAL,
    local_vt_session_identifier=1,
)
remote_session_id = ack.remote_vt_session_identifier

# Send data
data_ack = await client.vt_data(
    "192.168.1.100",
    vt_session_identifier=remote_session_id,
    vt_new_data=b"show status\r\n",
)
# data_ack.all_new_data_accepted indicates if device accepted all bytes
# data_ack.accepted_octet_count is the number of bytes accepted

# Close the session
await client.vt_close(
    "192.168.1.100",
    session_identifiers=[remote_session_id],
)

Supported VT classes: DEFAULT_TERMINAL, ANSI_X3_64, DEC_VT52, DEC_VT100, DEC_VT220, HP_700_94, IBM_3130.

List Element Operations

AddListElement and RemoveListElement (Clause 15.1–15.2) modify list-type properties without replacing the entire list. This is useful for managing recipient lists, object references, and other collection properties.

from bac_py.encoding.primitives import encode_application_unsigned

# Add elements to a list property
await client.add_list_element(
    "192.168.1.100",
    "notification-class,1",
    "recipient-list",
    list_of_elements=encode_application_unsigned(5),  # application-tagged
)

# Remove elements from a list property
await client.remove_list_element(
    "192.168.1.100",
    "notification-class,1",
    "recipient-list",
    list_of_elements=encode_application_unsigned(5),
)

The list_of_elements parameter takes pre-encoded bytes with application-tagged values. Use the encoding primitives from bac_py.encoding.primitives to build them.

An optional array_index parameter targets a specific element within an array-of-lists property.

Hierarchy Traversal

traverse_hierarchy() walks a StructuredView object tree by recursively reading Subordinate_List properties. It returns a flat list of all object identifiers found in the hierarchy.

# Get all objects under a structured view
all_objects = await client.traverse_hierarchy(
    "192.168.1.100",
    "structured-view,1",
    max_depth=10,
)
for oid in all_objects:
    print(oid)

The max_depth parameter (default 10) limits recursion to prevent infinite loops in misconfigured hierarchies. StructuredView objects found during traversal are descended into; other object types are collected as leaf nodes.

Protocol-Level API

For full control, use the protocol-level methods that accept typed objects instead of strings. These are available on both Client and BACnetClient.

from bac_py.network.address import parse_address
from bac_py.types.enums import ObjectType, PropertyIdentifier
from bac_py.types.primitives import ObjectIdentifier
from bac_py.encoding.primitives import encode_application_real
from bac_py.services.property_access import ReadAccessSpecification

addr = parse_address("192.168.1.100")
oid = ObjectIdentifier(ObjectType.ANALOG_INPUT, 1)

# ReadProperty -- returns ReadPropertyACK with raw property_value bytes
ack = await client.read_property(addr, oid, PropertyIdentifier.PRESENT_VALUE)

# WriteProperty -- value must be application-tagged encoded bytes
await client.write_property(
    addr, oid, PropertyIdentifier.PRESENT_VALUE,
    value=encode_application_real(25.0),
    priority=8,
)

# ReadPropertyMultiple -- full control over access specifications
specs = [ReadAccessSpecification(
    object_identifier=oid,
    list_of_property_references=[
        PropertyIdentifier.PRESENT_VALUE,
        PropertyIdentifier.STATUS_FLAGS,
    ],
)]
rpm_ack = await client.read_property_multiple(addr, specs)

# ReadRange -- read trend log buffer entries
from bac_py.services.read_range import RangeByPosition
rr_ack = await client.read_range(
    addr,
    ObjectIdentifier(ObjectType.TREND_LOG, 1),
    PropertyIdentifier.LOG_BUFFER,
    range_qualifier=RangeByPosition(reference_index=1, count=100),
)

See Two API Levels for guidance on when to use each level, and Server Mode for building servers with BACnetApplication directly.