Discovery and Networking

Device Discovery

Discover all devices

discover() sends a Who-Is broadcast and returns DiscoveredDevice objects with the responding device’s address, instance number, vendor ID, max APDU length, and segmentation support:

from bac_py import Client

async with Client(instance_number=999) as client:
    devices = await client.discover(timeout=3.0)

    print(f"Found {len(devices)} device(s):")
    for dev in devices:
        print(f"  Instance: {dev.instance}")
        print(f"  Address:  {dev.address_str}")
        print(f"  Vendor:   {dev.vendor_id}")
        print(f"  Max APDU: {dev.max_apdu_length}")
        print(f"  Segmentation: {dev.segmentation_supported}")

Discover devices in a range

Use low_limit and high_limit to narrow the search to a specific instance range:

devices = await client.discover(low_limit=100, high_limit=200, timeout=3.0)

Get a device’s object list

get_object_list() reads the complete list of objects from a remote device:

objects = await client.get_object_list("192.168.1.100", device_instance=100)
for obj_id in objects:
    print(f"  {obj_id.object_type.name},{obj_id.instance_number}")

Extended discovery

discover_extended() enriches standard discovery with Annex X profile metadata (Profile_Name, Profile_Location, Tags):

devices = await client.discover_extended(timeout=3.0, enrich_timeout=5.0)
for dev in devices:
    print(f"  Device {dev.instance}: profile={dev.profile_name}")
    if dev.tags:
        print(f"    tags: {dev.tags}")

Discover unconfigured devices

discover_unconfigured() listens for Who-Am-I broadcasts from unconfigured devices (Clause 19.7). This is useful for commissioning new devices that have not yet been assigned an instance number:

devices = await client.discover_unconfigured(timeout=5.0)
for dev in devices:
    print(f"  Vendor: {dev.vendor_id}  Model: {dev.model_name}")
    print(f"  Serial: {dev.serial_number}  Address: {dev.address}")

Find objects by identifier or name

who_has() sends a Who-Has broadcast to find devices that contain a specific object. Accepts string object identifiers:

results = await client.who_has(object_identifier="ai,1", timeout=3.0)
for r in results:
    print(f"  Device {r.device_identifier} has {r.object_identifier}")

# Or search by name
results = await client.who_has(object_name="Zone Temp", timeout=3.0)

Foreign Device Registration

To communicate across subnets, register as a foreign device with a BBMD (BACnet Broadcast Management Device). See BBMD Support for background on BBMD capabilities.

from bac_py import Client

async with Client(
    instance_number=999,
    bbmd_address="192.168.1.1",
    bbmd_ttl=60,
) as client:
    print(f"Status: {client.foreign_device_status}")

    # Discover devices on the BBMD's network
    devices = await client.discover(timeout=5.0)
    print(f"Discovered {len(devices)} device(s)")

    # Read BDT and FDT tables from the BBMD
    bdt = await client.read_bdt("192.168.1.1")
    for entry in bdt:
        print(f"  BDT: {entry.address} mask={entry.mask}")

    fdt = await client.read_fdt("192.168.1.1")
    for entry in fdt:
        print(f"  FDT: {entry.address} ttl={entry.ttl}s remaining={entry.remaining}s")

When bbmd_address is set, the client automatically registers on startup and re-registers before the TTL expires. You can also register manually at any time:

await client.register_as_foreign_device("192.168.1.1", ttl=60)

IPv6 Transport (Annex U)

bac-py supports BACnet/IPv6 per ASHRAE 135-2020 Annex U. Set ipv6=True on the Client or DeviceConfig to use IPv6 multicast instead of IPv4 broadcast:

from bac_py import Client

async with Client(ipv6=True) as client:
    devices = await client.discover(timeout=5.0)

The default multicast group is ff02::bac0 and the default interface is :: (all IPv6 interfaces). Both can be customized:

async with Client(
    ipv6=True,
    interface="fd00::1",
    multicast_address="ff05::bac0",  # site-local scope
) as client:
    ...

IPv6 foreign device registration works the same as IPv4, but uses bracket notation for the BBMD address:

async with Client(
    ipv6=True,
    bbmd_address="[fd00::1]:47808",
    bbmd_ttl=60,
) as client:
    devices = await client.discover(timeout=5.0)

For router-mode configurations, individual ports can use IPv6:

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,
                             ipv6=True, port=47808),
        ],
        application_port_id=0,
    ),
)

Router Discovery

Discover routers and the remote networks they can reach:

async with Client(instance_number=999) as client:
    routers = await client.who_is_router_to_network(timeout=3.0)

    for router in routers:
        print(f"Router at {router.address}:")
        print(f"  Networks: {router.networks}")

    # Discover devices on a remote network through a router
    if routers:
        remote_net = routers[0].networks[0]
        devices = await client.discover(destination=f"{remote_net}:*", timeout=5.0)
        for dev in devices:
            print(f"  Device {dev.instance} at {dev.address_str}")

See Addressing for the routed address format.

Multi-Network Routing

Configure bac-py as a BACnet router between multiple IP networks. See Network Routing for more on routing capabilities.

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