Network¶
The network layer handles NPDU encoding/decoding, address parsing, and multi-port routing. It sits between the application layer (APDUs) and the transport layer (raw datagrams).
For address format details, see Addressing. For router configuration, see BACnet/IP Router.
NPDU encoding/decoding, BACnet addressing, and network-layer management.
This package provides:
NetworkSender— protocol satisfied by bothNetworkLayer(non-router) andNetworkRouter(router mode) for sending APDUs.NetworkLayer— bridges transport and application layers in non-router mode, handling NPDU wrapping/unwrapping.NetworkRouter— full BACnet router engine per Clause 6.6.BACnetAddress/NPDU— addressing and PDU dataclasses.
Addressing¶
BACnet addressing types per ASHRAE 135-2020 Clause 6.
- class bac_py.network.address.BIPAddress(host, port)[source]¶
Bases:
objectA 6-octet BACnet/IP address composed of a 4-byte IPv4 address and a 2-byte UDP port.
Used as the MAC-layer address for BACnet/IP data links (Annex J).
- encode()[source]¶
Encode this address to the 6-byte BACnet/IP wire format.
- Return type:
- Returns:
A 6-byte
bytesobject (4 octets IP + 2 octets port, big-endian).
- classmethod decode(data)[source]¶
Decode a
BIPAddressfrom the 6-byte BACnet/IP wire format.- Parameters:
data (
bytes|memoryview) – At least 6 bytes of raw address data.- Return type:
- Returns:
The decoded
BIPAddress.- Raises:
ValueError – If data is shorter than 6 bytes.
- classmethod from_dict(data)[source]¶
Reconstruct a
BIPAddressfrom a dictionary produced byto_dict().- Parameters:
data (
dict[str,Any]) – Dictionary containing"host"and"port"keys.- Return type:
- Returns:
The reconstructed
BIPAddress.
- class bac_py.network.address.BIP6Address(host, port)[source]¶
Bases:
objectAn 18-octet BACnet/IPv6 address: 16-byte IPv6 + 2-byte UDP port.
Used as the MAC-layer address for BACnet/IPv6 data links (Annex U).
- encode()[source]¶
Encode to 18-byte wire format (16 octets IPv6 + 2 octets port, big-endian).
- Return type:
- classmethod decode(data)[source]¶
Decode from 18-byte wire format.
- Raises:
ValueError – If data is shorter than 18 bytes.
- Return type:
- Parameters:
data (bytes | memoryview)
- classmethod from_dict(data)[source]¶
Reconstruct a
BIP6Addressfrom a dictionary produced byto_dict().- Return type:
- Parameters:
- class bac_py.network.address.EthernetAddress(mac)[source]¶
Bases:
objectA 6-octet IEEE 802 MAC address for BACnet Ethernet (Clause 7).
Used as the MAC-layer address for BACnet/Ethernet (ISO 8802-3) data links.
- Parameters:
mac (bytes)
- classmethod decode(data)[source]¶
Decode from 6 bytes of MAC address data.
- Parameters:
data (
bytes|memoryview) – At least 6 bytes of raw address data.- Return type:
- Returns:
The decoded
EthernetAddress.
- classmethod from_dict(data)[source]¶
Reconstruct from a dictionary produced by
to_dict().- Parameters:
- Return type:
- Returns:
The reconstructed
EthernetAddress.
- class bac_py.network.address.BACnetAddress(network=None, mac_address=b'')[source]¶
Bases:
objectA full BACnet address: optional network number + MAC address.
Network numbers must be
None(local), 0xFFFF (global broadcast), or 1-65534 (valid remote network per Clause 6.2.1).- property is_broadcast: bool¶
Trueif this is any type of broadcast address (global, remote, or local).
- property is_remote_broadcast: bool¶
True if this is a broadcast on a specific remote network.
A remote broadcast has a network number set (not global 0xFFFF) and an empty MAC address (DLEN=0).
- classmethod from_dict(data)[source]¶
Reconstruct a
BACnetAddressfrom a dictionary produced byto_dict().- Parameters:
data (
dict[str,Any]) – Dictionary with optional"network"and"mac_address"keys.- Return type:
- Returns:
The reconstructed
BACnetAddress.
- bac_py.network.address.remote_broadcast(network)[source]¶
Create a remote broadcast address for a specific network.
A remote broadcast has the DNET set and an empty MAC address (DLEN=0).
- Parameters:
network (
int) – The target network number (1–65534).- Return type:
- Returns:
A
BACnetAddressrepresenting a directed broadcast on network.
- bac_py.network.address.remote_station(network, mac)[source]¶
Create a unicast address for a station on a remote network.
- Parameters:
- Return type:
- Returns:
A
BACnetAddresswith both DNET and DADR set.
- bac_py.network.address.parse_address(addr)[source]¶
Parse a human-readable address string to a BACnetAddress.
Accepted formats:
"192.168.1.100" -> local BACnet/IP, default port 0xBAC0 "192.168.1.100:47809" -> local BACnet/IP, explicit port "2:192.168.1.100" -> remote network 2, default port "2:192.168.1.100:47809" -> remote network 2, explicit port "[::1]" -> local BACnet/IPv6, default port 0xBAC0 "[::1]:47808" -> local BACnet/IPv6, explicit port "2:[::1]:47808" -> remote network 2, IPv6 "AA:BB:CC:DD:EE:FF" -> local Ethernet MAC "2:AA:BB:CC:DD:EE:FF" -> remote Ethernet MAC on network 2 "4352:01" -> remote station with hex MAC (e.g. MS/TP) "*" -> global broadcast "2:*" -> remote broadcast on network 2
If already a
BACnetAddress, returns it unchanged (pass-through).String results are cached so repeated lookups of the same address (common in building polling loops) are O(1) after the first call.
- Parameters:
addr (
str|BACnetAddress) – Address string or existingBACnetAddress.- Return type:
- Returns:
The parsed
BACnetAddress.- Raises:
ValueError – If the format is not recognised.
NPDU¶
NPDU encoding and decoding per ASHRAE 135-2016 Clause 6.
- class bac_py.network.npdu.NPDU(version=1, is_network_message=False, expecting_reply=False, priority=NetworkPriority.NORMAL, destination=None, source=None, hop_count=255, message_type=None, vendor_id=None, apdu=b'', network_message_data=b'')[source]¶
Bases:
objectDecoded Network Protocol Data Unit (Clause 6.2).
Represents the complete contents of a BACnet NPDU including the control octet fields, optional source/destination addressing, and either an application-layer APDU or a network-layer message payload.
- Parameters:
version (int)
is_network_message (bool)
expecting_reply (bool)
priority (NetworkPriority)
destination (BACnetAddress | None)
source (BACnetAddress | None)
hop_count (int)
message_type (int | None)
vendor_id (int | None)
apdu (bytes)
network_message_data (bytes)
- priority: NetworkPriority¶
Message priority (NORMAL, URGENT, etc.).
- destination: BACnetAddress | None¶
Remote destination address, or
Nonefor local.
- source: BACnetAddress | None¶
Originating address (populated by routers).
- bac_py.network.npdu.encode_npdu(npdu)[source]¶
Encode an
NPDUdataclass into on-the-wire bytes.Builds the version octet, control octet, optional destination/source address fields, hop count, and either the network-message type + data or the application-layer APDU payload.
Pre-calculates the total buffer size upfront and fills with slice assignment /
struct.pack_intoto avoid repeatedappend/extend.- Parameters:
- Return type:
- Returns:
The fully encoded NPDU byte string.
- Raises:
ValueError – If source address fields are invalid per the BACnet specification (e.g. SNET is 0xFFFF or SLEN is 0).
- bac_py.network.npdu.decode_npdu(data)[source]¶
Decode raw bytes into an
NPDUdataclass.Parses the version octet, control octet, optional destination/source address fields, hop count, and the remaining payload (network message or application-layer APDU).
- Parameters:
data (
memoryview|bytes) – Raw NPDU bytes (at least 2 bytes required).- Return type:
- Returns:
The decoded
NPDU.- Raises:
ValueError – If the data is too short or the protocol version is not 1.
- bac_py.network.npdu.encode_npdu_local_delivery(npdu, source_network, source_mac)[source]¶
Fast-path encode for router local delivery (no destination).
Combines NPDU construction and encoding into a single operation, skipping intermediate
NPDUobject creation. Used by the router when delivering to a directly-connected network: strips the destination and injects the source address in one step.
Network Messages¶
Network layer message encoding and decoding per ASHRAE 135-2024 Clause 6.4.
This module handles the variable-length data payloads of router-related network layer messages. The NPDU envelope (version, control, addresses, hop count, message type) is handled by npdu.py; this module handles only the data that follows the message type byte.
- class bac_py.network.messages.WhoIsRouterToNetwork(network=None)[source]¶
Bases:
objectClause 6.4.1 – request routing info for a specific or all networks.
If network is None, the request is for all reachable networks.
- Parameters:
network (int | None)
- class bac_py.network.messages.IAmRouterToNetwork(networks)[source]¶
Bases:
objectClause 6.4.2 – list of reachable DNETs.
- class bac_py.network.messages.ICouldBeRouterToNetwork(network, performance_index)[source]¶
Bases:
objectClause 6.4.3 – half-router advertisement.
Performance index: lower value = higher performance.
- class bac_py.network.messages.RejectMessageToNetwork(reason, network)[source]¶
Bases:
objectClause 6.4.4 – routing rejection with reason code.
- Parameters:
reason (RejectMessageReason)
network (int)
- reason: RejectMessageReason¶
- class bac_py.network.messages.RouterBusyToNetwork(networks)[source]¶
Bases:
objectClause 6.4.5 – congestion control, impose.
Empty networks list means all networks served by the router.
- class bac_py.network.messages.RouterAvailableToNetwork(networks)[source]¶
Bases:
objectClause 6.4.6 – congestion control, lift.
Empty networks list means all previously curtailed networks.
- class bac_py.network.messages.RoutingTablePort(network, port_id, port_info=b'')[source]¶
Bases:
objectA single port entry within an Initialize-Routing-Table message (Clause 6.5.5).
- class bac_py.network.messages.InitializeRoutingTable(ports)[source]¶
Bases:
objectClause 6.4.7 – routing table initialization or query.
Empty ports list (Number of Ports = 0) is a query requesting the complete routing table without modification.
- Parameters:
ports (tuple[RoutingTablePort, ...])
- ports: tuple[RoutingTablePort, ...]¶
- class bac_py.network.messages.InitializeRoutingTableAck(ports)[source]¶
Bases:
objectClause 6.4.8 – routing table initialization response.
Contains routing table data when responding to a query. Empty ports list when acknowledging an update.
- Parameters:
ports (tuple[RoutingTablePort, ...])
- ports: tuple[RoutingTablePort, ...]¶
- class bac_py.network.messages.EstablishConnectionToNetwork(network, termination_time)[source]¶
Bases:
objectClause 6.4.9 – instruct half-router to establish PTP connection.
Termination time of 0 means the connection is permanent.
- class bac_py.network.messages.DisconnectConnectionToNetwork(network)[source]¶
Bases:
objectClause 6.4.10 – instruct half-router to disconnect PTP connection.
- Parameters:
network (int)
- class bac_py.network.messages.WhatIsNetworkNumber[source]¶
Bases:
objectClause 6.4.19 – request local network number. No payload.
- class bac_py.network.messages.NetworkNumberIs(network, configured)[source]¶
Bases:
objectClause 6.4.20 – announce local network number.
configured=True means the number was manually configured. configured=False means it was learned from another device.
- bac_py.network.messages.encode_network_message(msg)[source]¶
Encode the data payload of a network layer message.
This encodes only the variable-length data that follows the message type byte in the NPDU. The caller is responsible for constructing the full NPDU with the correct message type.
Some message types (e.g.
WhatIsNetworkNumber) have no payload, so this function returnsb"".See
decode_network_message()for the inverse operation.- Parameters:
msg (
WhoIsRouterToNetwork|IAmRouterToNetwork|ICouldBeRouterToNetwork|RejectMessageToNetwork|RouterBusyToNetwork|RouterAvailableToNetwork|InitializeRoutingTable|InitializeRoutingTableAck|EstablishConnectionToNetwork|DisconnectConnectionToNetwork|WhatIsNetworkNumber|NetworkNumberIs) – A network message dataclass instance.- Return type:
- Returns:
Encoded data bytes (may be empty for some message types).
- Raises:
TypeError – If the message type is not recognized.
- bac_py.network.messages.decode_network_message(message_type, data)[source]¶
Decode the data payload of a network layer message.
See
encode_network_message()for the inverse operation.- Parameters:
message_type (
int) – TheNetworkMessageTypevalue from the NPDU.data (
bytes|memoryview) – The raw data bytes following the message type byte (may be empty for message types with no payload).
- Return type:
WhoIsRouterToNetwork|IAmRouterToNetwork|ICouldBeRouterToNetwork|RejectMessageToNetwork|RouterBusyToNetwork|RouterAvailableToNetwork|InitializeRoutingTable|InitializeRoutingTableAck|EstablishConnectionToNetwork|DisconnectConnectionToNetwork|WhatIsNetworkNumber|NetworkNumberIs- Returns:
A decoded network message dataclass instance.
- Raises:
ValueError – If the message type is not supported or data is malformed.
Network Layer¶
Network layer manager wiring transport to application per Clause 6.
Provides NetworkLayer, the non-router network layer manager that
bridges a single transport (any TransportPort
implementation) with the application layer by handling NPDU
wrapping/unwrapping, router cache management, and network-number learning.
- class bac_py.network.layer.RouterCacheEntry(network, router_mac, last_seen)[source]¶
Bases:
objectCached router address for a remote network.
Non-router devices maintain a cache mapping remote DNETs to the local MAC address of a router that can reach that network.
- class bac_py.network.layer.NetworkLayer(transport, network_number=None, *, network_number_configured=False, cache_ttl=300.0)[source]¶
Bases:
objectNetwork layer manager (non-router mode).
Bridges the transport layer and the application layer. Accepts any transport implementing
TransportPort(BACnet/IP, BACnet/IPv6, Ethernet, etc.). Handles NPDU wrapping/unwrapping for application-layer APDUs.Supports optional network number assignment and maintains a router cache for addressing remote networks via known routers.
- Parameters:
transport (TransportPort)
network_number (int | None)
network_number_configured (bool)
cache_ttl (float)
- on_receive(callback)[source]¶
Register a callback for received application-layer APDUs.
- Parameters:
callback (
Callable[[bytes,BACnetAddress],None]) – Called with(apdu_bytes, source_address)for each received NPDU containing an application-layer APDU.- Return type:
- register_network_message_handler(message_type, handler)[source]¶
Register a handler for incoming network-layer messages.
Multiple handlers may be registered for the same message type; they are invoked in registration order.
- unregister_network_message_handler(message_type, handler)[source]¶
Remove a previously registered network-layer message handler.
- send_network_message(message_type, data, destination=None)[source]¶
Send a network-layer message (non-APDU).
- Parameters:
message_type (
int) – Network message type code.data (
bytes) – Encoded message payload.destination (
BACnetAddress|None(default:None)) – Target address. IfNone, broadcasts locally. A local-network address (no DNET) sends a unicast without NPDU destination routing.
- Return type:
- send(apdu, destination, *, expecting_reply=True, priority=NetworkPriority.NORMAL)[source]¶
Send an APDU to a destination address.
Wraps the APDU in an
NPDUand sends via the transport layer. For remote destinations (DNET set), uses the router cache to send via a known router, or broadcasts if no router is cached.- Parameters:
apdu (
bytes) – Application-layer PDU bytes.destination (
BACnetAddress) – TargetBACnetAddress.expecting_reply (
bool(default:True)) – Whether a reply is expected (affects routing).priority (
NetworkPriority(default:<NetworkPriority.NORMAL: 0>)) – Network priority level.
- Return type:
- property local_address: object¶
The local address of the underlying transport.
Returns the transport-specific address type (e.g.
BIPAddressfor BACnet/IP,BIP6Addressfor BACnet/IPv6).
- add_route(network, router_mac)[source]¶
Pre-populate the router cache for a remote network.
Allows sending to a remote network without broadcast-based router discovery. Useful in environments (e.g. Docker bridge networks) where broadcast-based discovery is unreliable.
Router¶
Network router per ASHRAE 135-2024 Clause 6.
This module provides the routing table data structures and the
NetworkRouter engine that interconnects multiple BACnet
networks, forwarding NPDUs, processing network layer messages, and
maintaining the routing table dynamically.
- class bac_py.network.router.RouterPort(port_id, network_number, transport, mac_address, max_npdu_length, network_number_configured=True)[source]¶
Bases:
objectA single router port connecting to a BACnet network.
Each port represents one physical or logical attachment point of the router to a BACnet network. The port owns a
TransportPortthat handles the actual data-link send/receive.- Parameters:
- transport: TransportPort¶
The data-link transport for this port.
- class bac_py.network.router.RoutingTableEntry(network_number, port_id, next_router_mac=None, reachability=NetworkReachability.REACHABLE, busy_timeout_handle=None)[source]¶
Bases:
objectA single entry in the router’s routing table (Clause 6.6.1).
- Parameters:
network_number (int)
port_id (int)
next_router_mac (bytes | None)
reachability (NetworkReachability)
busy_timeout_handle (TimerHandle | None)
- reachability: NetworkReachability¶
Current reachability status.
- class bac_py.network.router.RoutingTable[source]¶
Bases:
objectRouter’s complete routing table (Clause 6.6.1).
Manages reachability information for all known networks. All mutating operations are synchronous and intended to be called from the asyncio event loop thread.
- add_port(port)[source]¶
Register a router port and create a directly-connected routing entry.
The port’s network_number is automatically added to the routing table as a directly-connected entry (no next-hop router).
- Parameters:
port (
RouterPort) – TheRouterPortto register.- Raises:
ValueError – If a port with the same port_id or a route for the same network_number already exists.
- Return type:
- get_port(port_id)[source]¶
Look up a
RouterPortby its identifier.- Parameters:
port_id (
int) – The port identifier to look up.- Return type:
- Returns:
The
RouterPort, orNoneif not found.
- get_all_ports()[source]¶
Return all registered
RouterPortinstances.- Return type:
- Returns:
A list of all ports managed by this routing table.
- get_port_for_network(dnet)[source]¶
Find the port and routing entry that can reach dnet.
- Parameters:
dnet (
int) – The destination network number to look up.- Return type:
- Returns:
A
(RouterPort, RoutingTableEntry)tuple, orNoneif no route to dnet is known.
- port_for_directly_connected(dnet)[source]¶
Return the port if dnet is directly connected, else
None.- Parameters:
dnet (
int) – The network number to check.- Return type:
- Returns:
The
RouterPortif the network is directly connected, orNoneif it is remote or unknown.
- get_reachable_networks(*, exclude_port=None, include_busy=False)[source]¶
Return network numbers of reachable routing table entries.
- Parameters:
exclude_port (
int|None(default:None)) – If given, exclude networks reachable through this port. Used when responding to Who-Is-Router so we don’t advertise networks back to the port that asked.include_busy (
bool(default:False)) – IfTrue, include networks marked BUSY (temporarily unreachable due to congestion). Per Clause 6.6.3.2, Who-Is-Router responses must include temporarily unreachable networks.
- Return type:
- Returns:
A list of reachable DNET numbers.
- get_all_entries()[source]¶
Return all routing table entries.
- Return type:
- Returns:
A list of all
RoutingTableEntryinstances.
- get_entry(dnet)[source]¶
Look up a routing table entry by network number.
- Parameters:
dnet (
int) – The destination network number to look up.- Return type:
- Returns:
The
RoutingTableEntry, orNoneif not found.
- update_route(dnet, port_id, next_router_mac)[source]¶
Add or update a routing table entry.
If the entry already exists, its port, next-hop, and reachability are updated. If the entry is new, it is created as REACHABLE.
- remove_entry(dnet)[source]¶
Remove a routing table entry.
Any pending busy timer is cancelled. Silently does nothing if the entry does not exist.
- update_port_network_number(port_id, new_network)[source]¶
Update a port’s network number and re-key its routing table entry.
Called when a Network-Number-Is message provides the actual network number for a port that was not statically configured. Updates both the port’s
network_numberand the routing table entry key so they remain consistent.- Parameters:
- Raises:
ValueError – If a route for new_network already exists (other than the port’s own entry).
- Return type:
- mark_busy(dnet, timeout_callback=None, *, timeout_seconds=30.0)[source]¶
Mark a network as BUSY (congestion control).
Starts a timer that will call timeout_callback after timeout_seconds (default 30 s per the BACnet specification). The callback is typically used to automatically restore the entry to REACHABLE.
Does nothing if the entry does not exist.
- class bac_py.network.router.NetworkRouter(ports, *, application_port_id=None, application_callback=None)[source]¶
Bases:
objectBACnet router engine per Clause 6.6.
Interconnects multiple BACnet networks via
RouterPortinstances. Forwards NPDUs between ports, processes network layer messages, and maintains the routing table dynamically.Optionally hosts a local application entity on one port, enabling the router device itself to participate in BACnet services.
- Parameters:
ports (list[RouterPort])
application_port_id (int | None)
application_callback (Callable[[bytes, BACnetAddress], None] | None)
- async start()[source]¶
Start all port transports, wire receive callbacks, and perform startup broadcasts.
Per Clause 6.6.2, after starting transports, each port receives:
A
Network-Number-Isbroadcast (if the port’s network number is configured).An
I-Am-Router-To-Networkbroadcast listing all networks reachable through other ports.
- Return type:
- property routing_table: RoutingTable¶
The router’s routing table.
- send(apdu, destination, *, expecting_reply=True, priority=NetworkPriority.NORMAL)[source]¶
Send an APDU from the router’s local application entity.
This is called by the application layer to send outbound messages. The router wraps the APDU in an NPDU and routes it to the appropriate port based on the destination address.
- Parameters:
apdu (
bytes) – Application-layer PDU bytes.destination (
BACnetAddress) – TargetBACnetAddress.expecting_reply (
bool(default:True)) – Whether a reply is expected.priority (
NetworkPriority(default:<NetworkPriority.NORMAL: 0>)) – Network priority level.
- Raises:
RuntimeError – If no application port is configured or the configured application port is not found.
- Return type: