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¶
String addresses ( |
|
String identifiers ( |
|
Auto-encodes Python values |
Pre-encoded |
Async context manager |
Requires a running
|
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 |
|
|
Write properties |
|
|
Device discovery |
|
|
Object search |
|
|
COV subscriptions |
|
|
Alarm management |
|
|
Event information |
|
|
Text messaging |
|
|
Object management |
|
|
Backup / restore |
|
|
Device control |
|
|
Time sync |
|
|
Audit log queries |
|
|
Foreign device |
|
|
Router discovery |
|
|
File access |
|
File Access (below) |
Private transfer |
|
Private Transfer (below) |
WriteGroup |
|
WriteGroup (below) |
Virtual terminal |
|
Virtual Terminal Sessions (below) |
List elements |
|
List Element Operations (below) |
Hierarchy traversal |
|
Hierarchy Traversal (below) |
Unconfigured devices |
|
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.