Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[1.5.7] - 2026-02-24

Fixed

  • Client decode bugs: Fixed five methods in app/client.py that performed isinstance checks on raw application-tagged bytes without first calling decode_and_unwrap() or decode_all_application_values(): _traverse_hierarchy_recursive, discover_extended enrichment, _discover_device_oid, _poll_backup_restore_state, and backup_device config file decoding. These could cause silent data misinterpretation or TypeError at runtime.

  • WritePropertyMultiple server decode: handle_write_property_multiple in app/server.py now calls decode_and_unwrap() on property values before writing, matching the pattern used elsewhere.

  • Segmentation double-counting: SegmentationManager._total_bytes no longer double-counts when a duplicate segment is received, which could cause premature reassembly completion.

  • Audit log query unbound variable: AuditLogQueryByTargetACK.decode() and AuditLogQueryBySourceACK.decode() in services/audit.py now initialize inner_end before the while loop, fixing an UnboundLocalError on empty inner sequences.

  • extract_context_value() closing tag validation: The function now verifies that the closing tag number matches the expected opening tag number, raising ValueError on mismatch instead of silently accepting malformed packets.

  • TSM falsy-value bug: Three places in app/tsm.py used x or default which incorrectly replaced valid falsy values (e.g., 0, 0.0) with defaults. Changed to explicit if x is not None checks.

  • Schedule engine midnight race: ScheduleEngine._evaluate_all now reads datetime.now() once per evaluation instead of calling date.today() and datetime.now().time() separately, preventing a rare midnight-crossing inconsistency.

Changed

  • LifeSafetyOperationRequest.decode tag class check: Added explicit TagClass.CONTEXT validation before decoding context tag 0, raising ValueError with a clear message if an application tag is encountered.

  • Decode assertions → ValueError: Replaced 10 bare assert statements in services/alarm_summary.py and services/event_notification.py decode paths with raise ValueError(...), ensuring decode errors are never silenced by -O optimization.

  • VT session hardening: Added source-address validation on VT-Close / VT-Data requests and a 64-session cap to prevent resource exhaustion via unbounded VT session creation.

  • Routing table cap: RoutingTable.update_route now enforces a 2048-entry limit, rejecting new entries when the table is full to prevent memory exhaustion from malicious route advertisements.

  • Address decode bounds checks: BIPAddress.decode and BIP6Address.decode now validate minimum buffer lengths (6 and 18 bytes respectively) before parsing, raising ValueError instead of producing truncated addresses.

  • BIBB conformance public API: ServiceRegistry now exposes has_confirmed_handler() and has_unconfirmed_handler() methods; conformance/bibb.py uses these instead of accessing private _confirmed / _unconfirmed dictionaries.

Removed

  • Dead code: Removed unused _min_unsigned_bytes() from encoding/primitives.py, superseded _MAX_SEGMENTS_DECODE / _MAX_APDU_DECODE dicts from encoding/apdu.py, and 84-line encode_npdu_with_source() from network/npdu.py.

  • Redundant branches: Eliminated no-op ternary and identical if/else branches in app/event_engine.py, and a redundant conditional in segmentation/manager.py handle_segment_ack.

  • Unused loggers: Removed 8 unused _logger definitions and their import logging statements from service modules (alarm_summary, object_mgmt, cov, virtual_terminal, read_property_multiple, write_property_multiple, write_group, audit).

[1.5.6] - 2026-02-23

Fixed

  • extract_context_value() Boolean True decode failure: Fixed a bug where extract_context_value() in encoding/tags.py failed with "Missing closing tag" when the context-wrapped data contained an application-tagged Boolean with value True. Per Clause 20.2.3, application-tagged Booleans encode the value in the tag’s L/V/T bits with zero content octets, but the function was treating the L/V/T value as a content byte count, advancing 1 byte too far and skipping past the closing tag. Boolean False was unaffected (LVT=0 happened to produce correct advancement). This caused any ReadProperty or ReadPropertyMultiple of a Boolean property set to True (e.g. log-enable, out-of-service, stop-when-full) to fail on the client side when decoding the response.

[1.5.5] - 2026-02-23

Changed

  • Flexible input types for protocol-level Client methods: read_property(), write_property(), and read_range() now accept str | BACnetAddress for addresses, str | tuple | ObjectIdentifier for object identifiers, and str | int | PropertyIdentifier for property identifiers – matching the convenience API (read(), write()). read_property_multiple() and write_property_multiple() now accept str | BACnetAddress for addresses. All parse functions use fast isinstance pass-through for already-typed objects, so existing code is fully backward-compatible with no performance overhead. The underlying BACnetClient retains strict-typed signatures as the intentional low-level API.

[1.5.4] - 2026-02-20

Added

  • json_default() function: Public default handler for serializing BACnet types with stdlib json.dumps() or orjson.dumps(). Handles objects with to_dict(), bytes/memoryview (→ hex string), and IntEnum subclasses (→ int). Importable from bac_py and bac_py.serialization.

  • write_multiple() priority parameter: Both BACnetClient.write_multiple() and Client.write_multiple() now accept an optional priority (1–16) that is applied uniformly to every property in the request.

[1.5.3] - 2026-02-18

Added

  • SC server example (examples/sc_server.py): BACnet/SC server using BACnetApplication and DefaultServerHandlers with an SC hub function for full APDU dispatch (ReadProperty, WriteProperty, Who-Is, etc.). This is the high-level approach to running an SC server.

  • IPv6 server example (examples/ipv6_server.py): BACnet/IPv6 server using BACnetApplication and DefaultServerHandlers.

  • Ethernet server example (examples/ethernet_server.py): BACnet Ethernet (Clause 7) server using BACnetApplication and DefaultServerHandlers with platform notes for Linux and macOS.

  • Docker SC server role (docker/entrypoint.py): New run_sc_server() function and "sc-server" role dispatch using BACnetApplication with SC transport, matching the pattern of run_server() and run_server_ipv6().

  • Client transport parameters: Client constructor now accepts sc_config, ethernet_interface, and ethernet_mac parameters for selecting BACnet/SC and Ethernet transports directly.

  • Top-level SC exports: SCHubConfig and SCTLSConfig are now importable directly from bac_py (lazy-loaded like SCTransport).

  • Top-level Ethernet export: EthernetTransport is now importable directly from bac_py.

Changed

  • Renamed ipv6_client_server.pyipv6_client.py: The example was client-only with no server component. Updated docstring to reference the new ipv6_server.py for the server counterpart.

  • Updated SC example docstrings: secure_connect.py and secure_connect_hub.py now clarify they are low-level examples and point to sc_server.py for the high-level BACnetApplication approach.

Documentation

  • User guide: transport-setup.rst — Added high-level DeviceConfig-based server examples for IPv6, Ethernet, and SC transports. Existing low-level transport examples retained with clarified headings.

  • User guide: secure-connect.rst — Added “High-Level Integration” section showing DeviceConfig(sc_config=...) and Client(sc_config=...). Clarified “Quick Start” and “Hub Function” sections as low-level API.

  • User guide: client-guide.rst — Added “Transport Options” section documenting Client constructor parameters for IPv6, SC, and Ethernet.

  • User guide: server-mode.rst — Added ipv6, sc_config, ethernet_interface, and ethernet_mac to the DeviceConfig Options section with mutual exclusion note.

  • User guide: getting-started.rst — Added transport selection examples to the Configuration section (IPv6, SC, Ethernet).

  • User guide: examples.rst — Updated example count (23 → 26), added documentation entries for sc_server.py, ipv6_server.py, and ethernet_server.py. Added Ethernet section.

  • README.md — Updated example table entry for renamed ipv6_client.py.

  • Test count: test_example_count updated to >= 25 (26 total examples).

[1.5.2] - 2026-02-16

Fixed

  • Docker entrypoint sys.path — Added project root to sys.path so docker.lib imports work when the entrypoint is run as a script.

  • SC PKI generate_test_pki() — Clear directory contents instead of shutil.rmtree() on Docker volume mount points (which cannot be removed).

  • SC certificate SANs — Use specific IPv4Address entries instead of IPv4Network for Docker bridge IPs in certificate Subject Alternative Names. IPv4Network does not work for SSL hostname verification.

Added

  • Mixed-environment SC profilingbench_sc.py supports --mode hub and --mode client for split Docker/local benchmarks, enabling isolated pyinstrument profiling of hub-side or client-side TLS overhead. --generate-certs DIR creates shared TLS certificates with broad SANs (localhost, host.docker.internal, Docker bridge IPs). New Docker Compose profiles sc-bench-hub and sc-bench-client and Makefile targets bench-sc-profile-client and bench-sc-profile-hub orchestrate the mixed-environment runs.

  • Docker scenario 14: Mixed BIP↔IPv6 routing — A BACnet/IP client on network 1 communicates with a BACnet/IPv6 server on network 2 through a dual-stack NetworkRouter. Tests read, write, RPM, WPM, and object-list operations through the cross-transport router (6 tests). make docker-test-mixed-bip-ipv6.

  • Docker scenario 15: Mixed BIP↔SC routing — A BACnet/IP client sends NPDUs through a BIP↔SC NetworkRouter to SC echo nodes connected via an SC hub with mutual TLS 1.3 on network 2. SC echo nodes parse incoming NPDUs and swap SNET/SADR→DNET/DADR headers for proper routed responses. TLS certificates are generated locally and bind-mounted into containers (4 tests). make docker-test-mixed-bip-sc.

  • Entrypoint roles: router-bip-sc, sc-npdu-echo — New Docker entrypoint roles for BIP↔SC gateway routing and NPDU-level SC echo with proper routing header manipulation.

Changed

  • Docker images tagged with version — All services share a version-tagged image (bac-py:<version>) for reproducible builds.

  • docker-build uses docker build directly — Replaced docker compose build (which built nothing since all services have profiles) with a direct docker build command. docker-clean now removes all bac-py:* images.

  • Performance: APDU dispatch optimization — Replaced match/case on PduType enum with direct isinstance checks in _on_apdu_received(), eliminating a redundant PduType extraction from the raw byte after decode_apdu() already determines the type. BIP throughput improved ~4%, Router throughput improved ~36% (cumulative with loop caching).

  • Performance: Event loop caching — Cache the running asyncio event loop in BACnetApplication.start() and use loop.create_task() instead of asyncio.create_task() in _spawn_task(), skipping the get_running_loop() lookup on every request dispatch.

  • Performance: SC WebSocket pending events deque — Changed SCWebSocket._pending_events from list with O(n) pop(0) to collections.deque with O(1) popleft() and built-in maxlen=64 cap.

  • Performance: SC hub payload skip — Hub connections now decode BVLC-SC messages with skip_payload=True, avoiding a bytes() copy of the NPDU payload that the hub never inspects (it forwards raw bytes directly).

[1.5.1] - 2026-02-15

Added

  • SC TLS stress testing: All BACnet/SC stress tests and benchmarks now use mutual TLS 1.3 with a mock CA (EC P-256) by default, matching production requirements (Annex AB.7.4). A shared docker/lib/sc_pki.py module generates the test PKI. Docker SC scenarios use init containers to generate certificates into shared volumes. The local bench_sc.py benchmark accepts --no-tls to fall back to plaintext for comparison.

  • add_route() API: Added add_route(network, router_address) to Client, BACnetApplication, and NetworkLayer for pre-populating the router cache. Enables communication with devices on remote networks without broadcast-based router discovery — required in Docker bridge networks where ephemeral-port clients cannot receive broadcast responses on the standard BACnet port.

Fixed

  • Router per-port broadcast address: Added broadcast_address field to RouterPortConfig and pass it to BIPTransport in router mode. Previously, all router ports used the default global broadcast (255.255.255.255), which fails in Docker bridge networks where directed subnet broadcasts are required. Docker router services now use BROADCAST_ADDRESS_1/BROADCAST_ADDRESS_2 environment variables for per-port configuration.

  • BIPTransport ephemeral port broadcast: Fixed BIPTransport.send_broadcast() sending to port 0 when the transport was created with port=0 (ephemeral). The bound port is now stored after socket binding so broadcasts and BBMD advertisements use the correct port.

  • Docker router stress test: Fixed the pre-existing router stress test failure caused by three compounding issues: (1) per-port broadcast addresses not being passed to the router’s BIPTransport, (2) ephemeral port clients unable to receive broadcast I-Am responses forwarded by the router, and (3) missing router cache pre-population. The test now uses add_route() and a direct server address (SERVER_ADDRESS) to bypass all broadcast-dependent discovery.

Security

  • SC hub VMAC collision race fix: Added _pending_vmacs reservation set to SCHubFunction to prevent a TOCTOU race between VMAC collision check and connection registration during the handshake window (Annex AB.6.2).

  • SC URI scheme validation: SCNodeSwitch.establish_direct() now validates that hub-provided peer URIs use ws:// or wss:// schemes before connecting, preventing SSRF-like redirection to non-WebSocket endpoints.

  • SC header options count cap: BVLC-SC header option decoding now limits lists to 32 options per message (defense-in-depth against malformed payloads).

  • SC pending resolution cache cap: SCNodeSwitch.resolve_address() now rejects new resolution requests when the cache reaches max_connections, preventing unbounded memory growth from address resolution flooding.

  • BBMD max BDT entries: Added max_bdt_entries parameter (default 128) to BBMDManager. Write-BDT requests exceeding the limit are NAKed, preventing oversized BDT payloads from consuming unbounded memory.

  • IPv6 VMAC cache size limit: VMACCache now accepts a max_entries parameter (default 4096). When full, stale entries are evicted first; if still full, the oldest entry is dropped.

  • IPv6 pending resolution cap: BIP6Transport.send_unicast() now limits the pending VMAC resolution cache to 1024 entries, preventing unbounded growth from resolution requests to many unknown VMACs.

  • H1: decode_real/decode_double buffer validation: Added explicit length checks before struct.unpack_from in encoding/primitives.py, raising ValueError instead of the opaque struct.error on truncated input.

  • H2: ErrorPDU bounds check: Added bounds checks after each decode_tag() in _decode_error() (encoding/apdu.py) to reject truncated error class/code fields before slicing.

  • H3: extract_context_value overflow check: Added bounds validation in encoding/tags.py to reject primitive tags whose length extends past the buffer end, preventing silent reads of stale/adjacent memory.

  • H4: Ethernet 802.3 minimum length: _decode_frame() in transport/ethernet.py now rejects frames with length field < LLC header size (3 bytes), preventing underflow in NPDU extraction.

  • C1: Service decoder list caps: Added _MAX_DECODED_ITEMS = 10,000 cap to all unbounded decode loops across 8 service files (19 loops total): read_property_multiple, write_property_multiple, alarm_summary, cov, write_group, virtual_terminal, object_mgmt, and audit.

  • C2: ObjectType vendor cache cap: ObjectType._missing_() now clears the vendor cache at 4096 entries, matching the PropertyIdentifier pattern and preventing unbounded growth from vendor-proprietary object types.

  • C3: Segmentation reassembly size cap: SegmentReceiver now tracks total reassembly bytes and returns ABORT when the cumulative size exceeds 1 MiB. Added created_at timestamp field for stale receiver detection.

  • C4: Audit nesting depth enforcement: Added depth checks (max 32) to all manual tag-nesting loops in services/audit.py (4 loops), preventing stack exhaustion from deeply nested opening tags in audit decode paths.

  • S1: Hub pending VMAC TTL and cap: Converted _pending_vmacs from set to dict[SCVMAC, float] with 30-second TTL purge and max_connections cap in transport/sc/hub_function.py, preventing unbounded growth from slow or abandoned handshakes.

  • S2: SC header option data size cap: Added _MAX_OPTION_DATA_SIZE = 512 limit to SCHeaderOption.decode_list() in transport/sc/bvlc.py, rejecting oversized option data (up to 65535 per option) early in the decode path.

  • S3: SC WebSocket oversized frame rate limit: SCWebSocket._process_frame() now tracks consecutive oversized frames and raises ConnectionClosedError after 3 in a row, preventing log flooding from misbehaving peers.

  • S4: SC WebSocket pending events cap: Capped _pending_events buffer at 64 entries in transport/sc/websocket.py, silently dropping excess frames when a single TCP segment delivers many WebSocket frames.

  • S5: SC address resolution URI cap: AddressResolutionAckPayload.decode() now truncates the URI list to 16 entries, preventing unbounded allocations from malformed address resolution responses.

  • B1: FDT TTL upper bound: Foreign device registration TTL is now capped at 3600 seconds (1 hour) in transport/bbmd.py, preventing unreasonably long registration durations.

  • A1: Change callback cap: ObjectDatabase.register_change_callback() now raises ValueError when a single property exceeds 100 registered callbacks, preventing unbounded list growth.

  • COV nesting depth enforcement: Added depth check (max 32) to the manual tag-nesting loop in COVPropertyValue.decode() (services/cov.py), preventing stack exhaustion from deeply nested opening tags in COV value decode paths.

  • decode_boolean buffer validation: Added explicit length check before accessing data[0] in decode_boolean() (encoding/primitives.py), raising ValueError on empty input for consistency with decode_real and decode_double.

[1.5.0] - 2026-02-15

Added

  • CONTRIBUTING.md: Contributing guidelines covering development setup, code standards, quality gates, and pull request process.

  • SECURITY.md: Security policy with vulnerability reporting instructions, supported versions, and security considerations for BACnet deployments.

  • CODE_OF_CONDUCT.md: Contributor Covenant 3.0 Code of Conduct.

  • GitHub issue templates: Structured bug report and feature request forms (.github/ISSUE_TEMPLATE/bug_report.yml, feature_request.yml).

  • GitHub PR template: Pull request template with quality gate checklist (.github/PULL_REQUEST_TEMPLATE.md).

  • README badges: Added PyPI version, Python version, license, and CI status badges. Added Contributing section linking to CONTRIBUTING.md and SECURITY.md.

  • Changelog project URL: Added Changelog link to [project.urls] in pyproject.toml for display on PyPI.

Changed

  • Release workflow: Restructured .github/workflows/release.yml into three separate jobs (release, build, publish) per the official Python packaging guide. The publish job uses a dedicated pypi GitHub Environment with id-token: write and attestations: write permissions for trusted publishing with PEP 740 attestations. Build artifacts pass between jobs via actions/upload-artifact.

  • sdist exclusions: Configured [tool.hatch.build.targets.sdist] to exclude docker/, scripts/, docs/, .github/, Makefile, ruff.toml, uv.lock, and .python-version from source distributions. Reduces sdist from 397 to 314 files. Wheel contents unchanged (only bac_py/ package).

Fixed

  • README test count: Reconciled inconsistent test counts (“6,380+” vs “6,300+”) in the Testing section.

[1.4.9] - 2026-02-14

Added

  • Interactive CLI example: New examples/interactive_cli.py providing a menu-driven interactive CLI for testing Client API features against a real BACnet device. Supports read/write (single and multiple), device discovery (Who-Is, Who-Has, object list), COV subscriptions with live notifications, and time synchronization. Uses non-blocking input to keep the event loop responsive for COV callbacks between actions.

[1.4.8] - 2026-02-14

Added

  • Expanded object type aliases: Added 48 short aliases (up from 10) for common object types in parse_object_identifier(). New aliases include file, nc, sched, tl, el, ch, lp, lo, sv, np, ee, tmr, iv, csv, acc, lc, avg, al, ar, and more. Full hyphenated names (e.g. "analog-input") continue to work without needing aliases.

  • Expanded property identifier aliases: Added 45 short aliases (up from 8) for common property identifiers in parse_property_identifier(). New aliases include type, list, priority, relinquish, min, max, polarity, event-state, high-limit, low-limit, deadband, notify-class, vendor-name, model-name, max-apdu, log-buffer, enable, and more.

Changed

  • traverse_hierarchy() string support: Now accepts string addresses and object identifiers (e.g. "sv,1") in addition to typed objects.

  • who_has() string support: object_identifier parameter now accepts string formats (e.g. "ai,1") in addition to ObjectIdentifier.

  • read_multiple() timeout parameter: Added missing timeout parameter for consistency with write_multiple() and other convenience methods.

Docs

  • Alias reference tables: Updated the string aliases tables in getting-started.rst, features.rst, and README.md to reflect the expanded alias sets. Fixed incorrect sf alias reference in README (correct alias is status).

[1.4.7] - 2026-02-14

Changed

  • Router local-delivery fast path: Added encode_npdu_local_delivery() that combines NPDU construction and encoding into a single operation, eliminating intermediate NPDU and BACnetAddress object creation on every routed packet in _deliver_to_directly_connected().

  • Router forwarding fast path: Added encode_npdu_with_source() combined encode for source-injected NPDUs with destination preserved.

  • Debug log guards (BIP transport): Added if __debug__ and logger.isEnabledFor() guards to send_unicast() and _on_datagram_received() hot paths, preventing string formatting and enum .name attribute access on every UDP datagram when DEBUG logging is disabled.

  • Debug log guards (network layer): Guarded logger.debug in _on_npdu_received() (every APDU dispatch) and _send_remote() (every remote send, avoids .hex() call).

  • Debug log guards (router): Guarded logger.debug in _forward_to_network() hot path (every directly-connected and next-hop forwarding decision).

Docs

  • Docstring cleanup: Added :param: documentation to all server handler methods (handle_read_property, handle_write_property, handle_subscribe_cov, etc.), AuditManager.record_operation(), COV manager methods, and BACnetApplication request methods.

  • Removed spec page/table references: Replaced all page number references (e.g. pp. 821-822), table references (e.g. Table 19-4, Table 6-1, Table 20.2.9.1), and figure references (e.g. Figure 6-11, Figure 6-12) with Clause references throughout the codebase. Clause references are retained.

[1.4.6] - 2026-02-14

Changed

  • SC BVLC decode performance: Replaced SCControlFlag IntFlag bitwise operations with raw integer constants and BvlcSCFunction enum construction with pre-built tuple lookup in the decode hot path, eliminating ~9% CPU overhead per message.

  • SCVMAC fast construction: Added SCVMAC._from_trusted() classmethod that bypasses length validation on internal decode paths where the caller guarantees 6 bytes.

  • NPDU decode performance: Added _make_npdu() fast constructor bypassing frozen dataclass __init__ overhead, and NetworkPriority tuple lookup replacing enum construction.

  • BVLL decode performance: Replaced BvlcFunction enum construction with pre-built tuple lookup indexed by byte value.

  • APDU decode performance: Replaced PduType enum construction with pre-built tuple lookup, added _make_confirmed_request() fast constructor for ConfirmedRequestPDU, and replaced dict.get() in max-segments/max-APDU decoding with direct tuple indexing.

  • TSM event loop caching: Cached asyncio.get_running_loop() result in both ClientTSM and ServerTSM to avoid repeated lookups on every timeout start.

  • Debug log guards: Added if __debug__ and logger.isEnabledFor() guards around debug logging in SC BVLC encode/decode, APDU encode/decode, SC transport send/receive, and SC hub function routing hot paths to avoid string formatting and attribute access when DEBUG is disabled.

  • NPDU encode performance: encode_npdu() now pre-calculates total buffer size and fills a single pre-sized bytearray with slice assignment and struct.pack_into, replacing repeated append()/extend() calls.

  • SCMessage fast construction: Added _make_sc_message() fast constructor bypassing frozen-dataclass __init__ overhead, used in SCMessage.decode().

  • BvllMessage fast construction: Added _make_bvll_message() fast constructor bypassing frozen-dataclass __init__ overhead, used in decode_bvll().

  • Router hot-path NPDU construction: _deliver_to_directly_connected() and _prepare_forwarded_npdu() now use _make_npdu() fast constructor instead of NPDU(...), avoiding frozen-dataclass overhead on every routed packet.

[1.4.5] - 2026-02-14

Added

  • Benchmark profiling: All local benchmark scripts (bench_bip, bench_router, bench_bbmd, bench_sc) accept --profile and --profile-html flags for pyinstrument profiling of async hot paths. pyinstrument added as a dev dependency. Makefile targets: make bench-{bip,router,bbmd,sc}-profile.

Changed

  • Docs dependency group: Added websockets, cryptography, and orjson to the docs dependency group so Sphinx autodoc builds with real imports instead of mock stubs, fixing SC transport API documentation warnings.

  • Transport setup guide: Removed redundant .. contents:: TOC directive (Furo theme provides sidebar navigation).

[1.4.4] - 2026-02-14

Changed

  • TLS X.509 strict verification: SSL contexts for BACnet/SC now enable VERIFY_X509_STRICT flag for stricter certificate validation per RFC 5280.

  • System CA store blocked: BACnet/SC SSL contexts no longer fall back to the system certificate store. Only explicitly configured CA certificates are trusted, preventing accidental trust of arbitrary CAs on the host.

  • Deprecated asyncio APIs replaced: All asyncio.ensure_future() calls replaced with asyncio.create_task() and all asyncio.get_event_loop() calls replaced with asyncio.get_running_loop() across the entire codebase (source, tests, examples, docker).

  • Fire-and-forget task error logging: Background tasks in BACnetApplication._spawn_task() and SC transport task schedulers now log exceptions via done callbacks instead of silently dropping them.

  • WebSocket client handshake timeout: SCWebSocket.connect() now accepts a handshake_timeout parameter (default 10s), matching the server-side accept() pattern. Prevents indefinite hangs on unresponsive peers.

Added

  • Private key passphrase support: SCTLSConfig.key_password parameter allows passphrase-protected PEM private keys (accepts bytes or str). Passphrase values are redacted in repr() alongside private_key_path.

  • Transport setup guide: New comprehensive documentation page covering all five transport types (BACnet/IP, IPv6, BBMD, Router, Ethernet, SC) with setup examples for common deployment topologies.

  • Documentation restructure: Moved changelog to its own sidebar section, added introductory text to all API reference pages, documented the IPv6 example script, and fixed the example count.

[1.4.3] - 2026-02-14

Changed

  • WebSocket write buffer tuning: SC WebSocket connections now set write buffer high/low water marks (32 KiB / 8 KiB) to trigger backpressure earlier for slow peers, and enable TCP_NODELAY for low-latency frame delivery.

  • WebSocket max_size enforcement: SCWebSocket.connect() and .accept() now accept a max_size parameter forwarded to the websockets protocol layer for early oversized-frame rejection. All callers (hub connector, hub function, node switch) pass max_bvlc_length as the limit.

  • Hub broadcast batched writes: SCHubFunction._broadcast() now buffers WebSocket frames to all connections synchronously, then drains them concurrently via asyncio.gather(), reducing broadcast latency.

  • WebSocket recv() event buffering: Fixed a bug where multiple WebSocket frames arriving in a single TCP segment could cause lost frames. recv() now buffers unconsumed events from events_received() for subsequent calls.

Added

  • Local SC benchmark: scripts/bench_sc.py runs a complete hub, echo nodes, and stress workers in a single process for Docker-free benchmarking. Supports human-readable and --json output modes. Makefile targets: make bench-sc and make bench-sc-json.

  • Local BIP benchmark: scripts/bench_bip.py runs an in-process BACnet/IP server with 40 objects and configurable stress client pools on localhost. Mixed workloads: read, write, RPM, WPM, object-list, COV. Makefile targets: make bench-bip and make bench-bip-json.

  • Local router benchmark: scripts/bench_router.py creates a two-network router on localhost with a stress server on network 2 and client pools on network 1. Measures cross-network routing overhead. Makefile targets: make bench-router and make bench-router-json.

  • Local BBMD benchmark: scripts/bench_bbmd.py runs a server with BBMD attached and foreign-device client pools. Includes FDT/BDT read workers. Makefile targets: make bench-bbmd and make bench-bbmd-json.

Documentation

  • Benchmarks guide overhaul (docs/guide/benchmarks.rst): Restructured into Local Benchmarks and Docker Benchmarks sections with reference results for all four transport types. Added results comparison table, key observations (local vs Docker performance characteristics), local CLI tuning parameters, and testing conditions.

[1.4.2] - 2026-02-14

Fixed

  • Receive callback crash protection: Wrapped 13 _receive_callback and application callback call sites in try/except across bip.py, bip6.py, sc/__init__.py, router.py, and layer.py. An exception from the callback no longer crashes the transport’s datagram handler or router receive loop. The ethernet transport already had this pattern; now all transports are consistent.

  • Assert → TypeError in encode paths: Replaced 8 assert isinstance() calls in BACnetTimeStamp.encode(), BACnetCalendarEntry.encode(), and BACnetValueSource.encode() with explicit TypeError raises. These validations now work correctly under python -O (optimized builds).

  • Router cache cap: NetworkLayer._router_cache is now capped at 1024 entries. When full, stale entries are evicted first, then the oldest entry. Prevents unbounded memory growth from I-Am-Router-To-Network floods.

  • Network list decode cap: _decode_network_list() now rejects messages containing more than 512 network numbers, preventing allocation of oversized tuples from malformed packets.

  • COV subscription cap: COVManager now accepts max_subscriptions and max_property_subscriptions parameters (default 1000). New subscriptions beyond the limit are rejected with BACnetError(RESOURCES).

  • Time series import cap: TimeSeriesImporter.from_json() and from_csv() now reject imports exceeding 100,000 records with a ValueError, preventing memory exhaustion from oversized files.

[1.4.1] - 2026-02-14

Changed

  • Encoding hot-path optimization: encode_unsigned() and encode_unsigned64() now use a pre-computed 256-element lookup table for values 0-255, eliminating to_bytes() allocation on the most common code path. encode_boolean() uses pre-allocated b"\x01" / b"\x00" constants.

  • Address encode caching: BIPAddress.encode() and BIP6Address.encode() now cache their result on the frozen dataclass instance, avoiding repeated inet_aton()/inet_pton() + struct.pack() calls. BIPAddress.decode() uses an LRU-cached factory to deduplicate instances for the same remote device.

  • Transport receive-path optimization: BIP and BIP6 transports now perform a fast self-echo check using raw tuple/VMAC comparison before allocating address objects, reducing per-packet overhead on broadcast-heavy networks.

  • BBMD forwarding optimization: BDT unicast-mask lookup is now O(1) via a pre-computed dict. Peer list excludes self, eliminating per-forward self-skip checks. Foreign device registration messages are pre-computed once in __init__ for both IPv4 and IPv6 managers.

  • ObjectType vendor member caching: ObjectType._missing_() now caches vendor-proprietary pseudo-members (128-1023), matching the existing PropertyIdentifier pattern. Repeated lookups return the same object.

  • StatusFlags singleton: A shared _NORMAL_STATUS_FLAGS instance is returned for the common all-normal case, avoiding per-read allocation.

  • standard_properties() caching: The 5-entry base property dict is now computed once and reused across all 40+ object class definitions.

Fixed

  • PropertyIdentifier vendor cache unbounded growth: The _PROPERTY_ID_VENDOR_CACHE dict is now capped at 4096 entries. A misbehaving device sending millions of unique vendor property IDs can no longer cause unbounded memory growth.

[1.4.0] - 2026-02-14

Added

  • Full BACnet/IPv6 (Annex U) integration — IPv6 transport is now fully wired into Client and BACnetApplication. Set ipv6=True to use IPv6 multicast discovery and communication:

    • Client(ipv6=True) creates a BIP6Transport with ff02::bac0 multicast

    • DeviceConfig(ipv6=True) and RouterPortConfig(ipv6=True) for app-level config

    • Mixed IPv4/IPv6 router ports in a single router configuration

    • IPv6 foreign device registration via bbmd_address="[fd00::1]:47808"

  • IPv6 BBMD manager (transport/bbmd6.py): BDT/FDT management, broadcast forwarding to peers and foreign devices, FDT expiry cleanup. Mirrors the IPv4 BBMD architecture adapted for Annex U (no broadcast mask, multicast callbacks, source VMAC on all messages).

  • IPv6 foreign device manager (transport/foreign_device6.py): Registration, re-registration loop at TTL/2, deregistration on stop, distribute-broadcast.

  • BVLL6 spec compliance fix: All 13 BVLL6 function codes now include source_vmac per Annex U (previously 4 codes were missing it).

  • NetworkLayer generalized to accept any TransportPort (not just BIPTransport), enabling IPv6 and future transport types.

  • IPv6 example script (examples/ipv6_client_server.py): Demonstrates IPv6 client with multicast discovery and property reads.

  • IPv6 Docker integration test (Scenario 13, profile ipv6): server-ipv6 and test-ipv6 containers on a fd00:bac:1::/64 network. New make docker-test-ipv6 target.

[1.3.11] - 2026-02-14

Added

  • Router stress test scenario (docker/scenarios/test_router_stress.py): Sustained cross-network routing throughput test. Discovers a server on a remote BACnet network through a router and runs mixed-workload stress workers (read, write, RPM, WPM, object-list) with all traffic traversing the router. Includes periodic route health-check workers. New make docker-test-router-stress and make docker-router-stress targets.

  • BBMD stress test scenario (docker/scenarios/test_bbmd_stress.py): Sustained foreign-device management throughput test. Registers test clients as foreign devices with a BBMD and runs mixed BACnet service workloads alongside BBMD-specific operations (FDT reads, BDT reads). Measures BBMD overhead under concurrent foreign device activity. New make docker-test-bbmd-stress and make docker-bbmd-stress targets.

  • Shared stress modules (docker/lib/router_stress.py, docker/lib/bbmd_stress.py): Reusable worker libraries extending bip_stress.Stats with routing-specific (RouterStats) and BBMD-specific (BBMDStats) metrics. Both modules reuse the core BIP stress workers for read/write/RPM/WPM operations.

  • Standalone stress runners (docker/lib/router_stress_runner.py, docker/lib/bbmd_stress_runner.py): JSON-reporting standalone runners for router and BBMD stress tests, following the same warmup/sustain/report pattern as the existing BIP and SC stress runners.

Changed

  • BACnet/SC WebSocket performance optimizations (transport/sc/): Multiple improvements to the SC transport hot path, collectively improving sustained throughput by ~25% and reducing p99 latency from 0.6ms to 0.4ms:

    • TCP_NODELAY on all SC connections (websocket.py): Disables Nagle’s algorithm on both client and server WebSocket connections. Prevents 40-200ms stalls when sending small BACnet frames (100-1500 bytes) due to Nagle + delayed-ACK interaction.

    • Raw bytes forwarding in hub (hub_function.py, connection.py): The hub now forwards pre-encoded message bytes directly to destination connections, skipping the decode-then-re-encode cycle for routed messages. The on_message callback chain passes raw wire bytes alongside the parsed SCMessage (optional raw: bytes | None parameter, backward-compatible).

    • BVLC header caching (__init__.py): SCTransport.send_unicast() and send_broadcast() use pre-computed BVLC-SC headers (per-destination for unicast, fixed for broadcast) concatenated with the NPDU payload, bypassing SCMessage object creation and encode() on every send.

    • Direct chunk writes (websocket.py): New _write_pending() helper writes protocol output chunks directly to the StreamWriter instead of collecting them with b"".join(), eliminating an allocation and copy per send.

    • Concurrent hub broadcasts (hub_function.py): Broadcast messages are forwarded to all connected nodes concurrently via asyncio.gather() instead of sequentially.

    • Pre-sized encode buffer (bvlc.py): SCMessage.encode() pre-calculates the total message size and allocates a single bytearray, eliminating incremental growth and reallocation. Added encode_encapsulated_npdu() fast-path function for the common case.

  • Cross-transport encoding optimizations: Applied the pre-sized bytearray pattern from BACnet/SC to all transport and encoding layers, reducing per-message memory allocations across the entire stack:

    • BVLL encode (bvll.py): encode_bvll() now uses a single pre-sized bytearray with struct.pack_into() instead of bytes([...]) + content concatenation. Eliminates 2 intermediate allocations per send for BACnet/IP, BBMD forwarding, and foreign device registration.

    • BVLL6 encode (bvll_ipv6.py): encode_bvll6() rewritten to calculate total message size upfront and fill a single bytearray, replacing the list.append() + b"".join() + header + content pattern that created 3+ intermediate allocations per send.

    • BIP send_unicast (bip.py): Inline MAC-to-host:port parsing avoids creating a BIPAddress object on every unicast send.

    • Ethernet frame encoding (ethernet.py): _encode_frame() uses a single pre-sized bytearray (zero-initialized for implicit padding) instead of concatenation + conditional padding allocation. LLC header validation simplified to a single 3-byte slice comparison. MAC address logging uses .hex(':') instead of generator + join.

    • NPDU logging guards (npdu.py): Debug log statements that call .hex() on MAC addresses are now guarded by logger.isEnabledFor(DEBUG), avoiding string formatting overhead on every encode/decode when debug logging is disabled.

    • Tag encoding lookup table (tags.py): Pre-computed 150-entry lookup table for the most common single-byte tag encodings (tag 0-14, length 0-4, both APPLICATION and CONTEXT classes). Eliminates bytes([...]) list allocation on ~95% of encode_tag() calls.

  • Benchmarks guide (docs/guide/benchmarks.rst): Added documentation for router stress and BBMD stress test configurations, architecture, and worker descriptions.

  • Docker infrastructure (docker-compose.yml, entrypoint.py, Makefile): Added Scenario 11 (Router Stress) and Scenario 12 (BBMD Stress) with dedicated server, router/BBMD, test, and runner containers. Added router-stress and bbmd-stress role dispatches in the entrypoint. Added 4 new Makefile targets.

[1.3.10] - 2026-02-14

Fixed

  • Missing docker/lib/ in repository (.gitignore): The lib/ gitignore pattern was excluding docker/lib/, which contains shared stress test worker modules (bip_stress.py, sc_stress.py, stress_runner.py, sc_stress_runner.py). Added !docker/lib/ negation so the directory is tracked. This caused mypy CI failures since docker.lib.* imports could not resolve.

[1.3.9] - 2026-02-14

Added

  • SC stress test scenario – New Docker-based sustained WebSocket throughput test (docker/scenarios/test_sc_stress.py) with unicast and broadcast workers through an SC hub, measuring latency with echo correlation. New make docker-test-sc-stress and make docker-sc-stress targets.

  • Benchmarks guide – New docs/guide/benchmarks.rst documenting both BIP and SC stress test configurations, server object inventories, workload profiles, latency targets, and tuning guidance.

Changed

  • DeviceConfig version defaults (application.py): firmware_revision and application_software_version now default to bac_py.__version__ instead of a hardcoded "0.1.0". Docker entrypoint and thermostat demo updated to use bac_py.__version__ instead of hardcoded version strings.

  • Docker build caching (Makefile): docker-build target now uses --no-cache to ensure clean builds.

  • BIP stress test refactored (docker/scenarios/test_stress.py, docker/entrypoint.py): Dedicated stress-server role with 40 diverse objects (analog, binary, multi-state, schedule, calendar, notification class). Configurable worker pools (readers, writers, RPM, WPM, object-list, COV) with environment variables. Warmup/sustain phase architecture replaces ramp schedule. Shared worker logic extracted to docker/lib/ modules; docker/__init__.py added to enable package imports from test scenarios.

  • Docker Compose reorganized (docker/docker-compose.yml): Added Scenario 10 (SC Stress) with hub, two echo nodes, test container, and stress runner. Stress runner container moved next to its server. Header comment with scenario index and usage guide.

Documentation

  • Server mode guide expansion (docs/guide/server-mode.rst): Expanded from 188 to 954 lines. Added sections for DeviceConfig options (password, broadcast_address, APDU settings), Object Database management (add/remove/query, change callbacks), supported object types (categorized list of 40+ types), commandable objects and priority arrays, COV subscriptions (server side), custom service handlers (signatures, registration, validation example, error responses), event engine (18 algorithms, intrinsic/algorithmic reporting), audit logging (server side), error handling (error table, password validation, DCC states), application lifecycle (context manager, manual, combined client+server), and registered services reference.

  • Client guide (docs/guide/client-guide.rst): New consolidated client reference page covering API level comparison, capabilities-at-a-glance table with cross-references, and previously undocumented features: file access (AtomicReadFile/AtomicWriteFile with stream and record examples), private transfer (confirmed/unconfirmed vendor-specific), WriteGroup (channel group writes), virtual terminal sessions (VT-Open/Data/Close), list element operations (AddListElement/RemoveListElement), hierarchy traversal (StructuredView walking), and protocol-level API examples.

  • Added guide/client-guide and guide/benchmarks to User Guide toctree in docs/index.rst.

  • Updated docs/getting-started.rst with cross-references to client guide and protocol-level API section.

  • Updated docs/features.rst with cross-references to client guide, commandable objects, supported object types, and new service documentation. Added SC Stress scenario, updated Docker scenario count from nine to ten, added benchmark cross-references.

[1.3.8] - 2026-02-14

Fixed

  • BIP future race condition (bip.py): Explicitly cancel pending BVLC request futures on timeout in _bvlc_request() to prevent late responses from setting results on garbage-collected futures. Also cancel all pending futures during stop() to prevent dangling references.

  • BIP6 memory leak (bip6.py): Capped pending address resolution queues at 16 entries per VMAC to prevent unbounded growth when resolution never completes. Added 30-second TTL eviction to discard stale queued NPDUs in _flush_pending().

  • BIP6 dangling futures (bip6.py): Cancel all pending BVLC futures and clear pending resolution queues during stop().

  • Ethernet exception handler (ethernet.py): Broadened _on_readable() exception catch from OSError to Exception to prevent unexpected parsing errors or callback exceptions from crashing the event loop reader.

  • Router assertion safety (router.py): Replaced three assert statements in _deliver_to_directly_connected(), _forward_via_next_hop(), and _send_reject_toward_source() with explicit guard checks that degrade gracefully when Python runs with -O optimization flag.

  • NPDU decode bounds checking (npdu.py): Added explicit bounds checks before reading DNET/DLEN, SNET/SLEN, hop count, and message type fields in decode_npdu() to raise ValueError instead of IndexError on truncated data.

  • Tag decode bounds checking (tags.py): Added bounds validation before reading extended tag numbers, 1/2/4-byte extended length fields. Truncated packets now raise ValueError with descriptive messages instead of IndexError.

  • Tag length allocation cap (tags.py): Reject tag lengths exceeding 1 MB (1,048,576 bytes) to prevent memory exhaustion from malformed or malicious packets.

  • Context nesting depth limit (tags.py): Cap context tag nesting at 32 levels in extract_context_value() to prevent stack exhaustion from crafted payloads.

  • Application value content bounds check (primitives.py): Added bounds validation in decode_application_value() after decoding tag metadata but before reading content bytes.

  • Decoded value count cap (primitives.py): decode_all_application_values() now limits to 10,000 values to prevent memory exhaustion from crafted payloads.

  • Constant-time password comparison (server.py): _validate_password() now uses hmac.compare_digest() instead of == to prevent timing-based password extraction attacks.

  • SC VMAC origin validation (hub_function.py): Hub function now validates that the originating VMAC in received messages matches the authenticated peer’s VMAC to prevent VMAC spoofing in hub-routed traffic (Annex AB.6.2).

  • SC TLS credential redaction (tls.py): SCTLSConfig.__repr__() now redacts private_key_path as '<REDACTED>' to prevent credential leaks in logs and tracebacks.

  • SC TLS configuration validation (tls.py): Added warnings for mismatched certificate_path/private_key_path configuration and missing CA certificates.

  • SC WebSocket frame size limit (websocket.py): Added max_frame_size parameter propagated from connection setup; oversized frames are logged at WARNING and dropped to prevent memory exhaustion.

  • SC plaintext warnings: Upgraded plaintext-mode log messages from DEBUG to WARNING across tls.py, __init__.py, hub_connector.py, hub_function.py, node_switch.py, and websocket.py with references to ASHRAE 135-2020 Annex AB.7.4.

Changed

  • Lazy logging across entire codebase: Converted all f-string logger.debug()/logger.warning()/logger.info() calls to lazy %s/%d formatting across the full stack: app/ (client, server, application, tsm, event_engine, cov, audit, schedule_engine, trendlog_engine), encoding/ (apdu), objects/ (base, device), segmentation/ (manager), serialization/, types/ (enums), transport/ (bip, bip6, bbmd, ethernet, npdu, layer, router, address), and transport/sc/ (all modules). This avoids string interpolation overhead when the log level is disabled.

  • Device info cache eviction (application.py): Capped _device_info_cache at 1,000 entries with FIFO eviction (removes oldest 100 when limit reached) to prevent unbounded growth from I-Am responses.

  • Application stop cleanup (application.py): Clear unconfirmed listeners and device info cache during stop() to release references.

  • Event engine stop cleanup (event_engine.py): Cancel pending confirmed notification tasks during stop() to release memory.

  • Server TSM buffer release (tsm.py): Clear cached_response on server transaction abort and timeout to release large byte buffers promptly.

  • SC transport stop cleanup (__init__.py): Cancel and clean up pending send tasks during stop() to prevent dangling references.

  • SC connection callback cleanup (connection.py): Clear on_connected, on_disconnected, on_message, and on_vmac_collision callbacks in _go_idle() to break reference cycles between connection, hub function, and node switch objects.

  • SC hub connector resource cleanup (hub_connector.py): Call _go_idle() on failed connections (VMAC collision or non-connected state) to clean up resources.

  • BBMD byte operations (bbmd.py): Replaced bytearray + extend() loops in _handle_read_bdt() and _handle_read_fdt() with b"".join() for fewer intermediate allocations.

  • Router cache optimization (layer.py): In _learn_router_from_source(), update last_seen timestamp in-place on fresh cache entries instead of creating a new RouterCacheEntry object.

  • Foreign device exception narrowing (foreign_device.py): Narrowed _registration_loop() exception catch from bare Exception to OSError.

  • Ethernet stop() cleanup (ethernet.py): Narrowed contextlib.suppress(Exception) to contextlib.suppress(OSError, ValueError) in stop().

  • Docker firmware/application version strings updated from "1.2.0" to "1.3.8".

Added

  • Security and memory safety documentation – New docs/guide/security.rst covering protocol safety (ASN.1/BER bounds checking, allocation caps, nesting depth limits), transport security (TLS 1.3, VMAC validation, frame size limits, credential redaction), logging safety (lazy formatting), memory safety (frozen dataclasses, bounded buffers, constant-time comparisons), dependency posture, and a production checklist.

  • ~1,000 new test lines covering security hardening: tag decode bounds, allocation caps, nesting depth, application value truncation, decoded value count limit, constant-time password comparison, SC VMAC origin validation, SC frame size limits, SC TLS credential redaction, SC plaintext warnings, NPDU truncation, Ethernet exception handling, and device info cache eviction.

Documentation

  • Added guide/security to the User Guide toctree in docs/index.rst.

  • Trimmed docs/guide/device-management.rst – removed JSON Serialization, Docker Integration Testing, and Protocol-Level API sections that were duplicated in features.rst and getting-started.rst.

  • Updated docs/features.rst Docker section: added SC scenario, updated count from eight to nine, added docker-test-sc target reference.

  • Fixed cross-references after removing protocol-level-api label.

[1.3.7] - 2026-02-14

Fixed

  • SC connection state machine (connection.py): Added re-entry guards to initiate() and accept() to prevent use from non-IDLE states. Fixed _go_idle() double-cancellation race when called from both disconnect() and background tasks. Wrapped on_message callback dispatch in try/except to prevent callback exceptions from killing the receive loop.

  • SC exception handling: Narrowed broad (TimeoutError, Exception) catches to specific (TimeoutError, OSError, ConnectionError) across connection.py, hub_connector.py, and node_switch.py to avoid catching SystemExit, KeyboardInterrupt, and other non-network exceptions.

  • SC node switch shutdown ordering: Reordered stop() to close direct connections before server.close() + wait_closed(), preventing Python 3.13 hang where wait_closed() blocks until active connection handlers finish.

Changed

  • SC BVLC-SC message encoding (bvlc.py): Replaced list-of-bytes + join pattern with pre-sized bytearray + struct.pack_into across all encode methods (SCMessage.encode(), SCHeaderOption.encode(), _encode_options(), BvlcResultPayload.encode()). Reduces allocations on the hot path.

  • SC payload consolidation: Merged identical ConnectRequestPayload and ConnectAcceptPayload into single _ConnectPayload with public aliases.

  • SC TLS context caching: Cached SSL contexts in SCHubConnector.__init__() and SCNodeSwitch.__init__() instead of rebuilding per connection attempt.

  • SC WebSocket accept timeout: Added handshake_timeout parameter (default 10s) to SCWebSocket.accept() to prevent slow-client denial of service.

  • Removed dead code (decode_list unreachable break) and moved lazy _WSState import to module level in websocket.py.

Added

  • Example test suite (tests/test_examples.py): 110 tests covering all 21 example scripts – syntax validation, module-level docstring checks, __main__ guard verification, async def main() presence, import validation (core and SC examples separately), and functional tests for create_object_database() and generate_test_pki() helpers.

  • Expanded CI quality gates: mypy type checking now covers src/, examples/, and docker/ (previously src/ only). Ruff lint and format checks now cover examples/. Added full type annotations to all 21 example scripts, 8 docker test scenarios, 2 demo scripts, the docker entrypoint, and the stress runner.

Fixed

  • secure_connect_hub.py example: Fixed AnalogInputObject present-value writes that failed because present-value is read-only per the spec unless out-of-service is True. Now sets OUT_OF_SERVICE = True before writing values.

[1.3.6] - 2026-02-14

Added

  • Comprehensive structured logging across the entire stack – Every module now uses logging.getLogger(__name__) for hierarchical logger namespaces under bac_py.*. Users can enable granular debugging by configuring any logger in the hierarchy (e.g., logging.getLogger("bac_py.app.client").setLevel(logging.DEBUG)).

    • app/client.py: 56 log statements across all public methods (DEBUG for request/response, INFO for lifecycle/discovery operations)

    • app/application.py: Lifecycle (INFO start/stop), APDU dispatch (DEBUG), device info cache updates, handler errors (ERROR with exc_info)

    • App engines (tsm, event, cov, audit, schedule, trendlog): Transaction lifecycle, event state transitions, COV subscription management, audit record creation, schedule/trend evaluation cycles

    • app/server.py: Handler dispatch (DEBUG), registration events, all error paths now log WARNING before raising BACnetError

    • Network layer (npdu, layer, router, address): NPDU encode/decode routing info, APDU dispatch, router cache updates, address parsing

    • Transports (bip, bbmd, ethernet, bip6): Send/receive (DEBUG), BBMD lifecycle (INFO start/stop), broadcast forwarding

    • Encoding/types (apdu, tags, enums, constructed): APDU encode/decode type identification, tag validation warnings, vendor-proprietary PropertyIdentifier creation, CHOICE decode failures

    • SC transport (all 8 files): Connection state machines, hub routing, failover events, TLS context creation, BVLC message codec, WebSocket connect/accept/close

    • Objects (base, device): ObjectDatabase add/remove (INFO), property read/write (DEBUG), not-found warnings

    • Segmentation: Segment send/receive progress, window management, transfer completion (INFO), duplicate/out-of-window warnings

    • Serialization: Serialize/deserialize operations (DEBUG), type errors (WARNING)

  • Debugging and logging documentation – New docs/guide/debugging-logging.rst guide with logger hierarchy table, log level descriptions, practical debugging recipes (failed reads, discovery, server handlers, segmentation, SC connections), file logging configuration, and performance notes. Added “Structured Logging” section to docs/features.rst and “Debugging and Logging” subsection to docs/getting-started.rst.

  • Logging in example scripts – Added logging.basicConfig() to 5 core examples (read_value.py, write_value.py, discover_devices.py, monitor_cov.py, object_management.py), bringing the total to 9 of 21 examples with logging setup.

[1.3.5] - 2026-02-14

Changed

  • ObjectDatabase Device object caching_increment_database_revision() now uses a cached reference to the Device object instead of scanning all objects on every add/remove/rename operation (O(1) instead of O(n)).

  • encode_property_value() dispatch table – Replaced 20-deep isinstance cascade for constructed BACnet types with an O(1) type-keyed dispatch table. The table is built lazily on first call to avoid circular imports. Primitive type dispatch retains the isinstance chain due to subclass ordering requirements (bool < int, IntEnum < int, BACnetDouble < float).

  • decode_real() / decode_double() use struct.unpack_from() – Avoids a memoryview slice copy on every float decode by using unpack_from instead of unpack with a slice. This is a hot path in APDU decoding.

  • client.py top-level imports – Moved 14 repeated local imports (parse_object_identifier, parse_property_identifier, _resolve_object_type, GLOBAL_BROADCAST, MessagePriority, EnableDisable, ReinitializedState) to module-level to eliminate per-call import lookup overhead.

  • Dispatch table encoders use b"".join() – Converted multi-part constructed type encoders (BACnetDestination, BACnetRecipientProcess, BACnetDeviceObjectPropertyReference, BACnetObjectPropertyReference, BACnetLogRecord) from O(n²) bytes += concatenation to O(n) b"".join().

  • encode_tag() fast path – Added a single-byte fast path for the common case (tag_number <= 14 and length <= 4), avoiding bytearray allocation entirely. This function is called 100+ times per APDU encode.

  • encode_npdu() removed redundant bytearray.clear() – The buffer was pre-allocated with an estimated size and then immediately cleared, defeating the pre-allocation. Now starts with an empty bytearray.

  • EventEngine deduplicated _sync_state_machine() calls – New enrollment contexts were synced twice on first evaluation (once in creation, once in the per-cycle sync). Removed the redundant initial sync.

  • Server _read_object_property() uses identity check – Replaced ObjectIdentifier.__eq__ comparison with obj is self._device identity check for the Device object special-case path.

  • Server _expand_property_references() uses short-circuit scan – Replaced set comprehension with any() generator for PROPERTY_LIST membership check, avoiding full set construction on every ReadPropertyMultiple ALL request.

  • Pre-computed opening/closing tag lookup tablesencode_opening_tag() and encode_closing_tag() now return pre-computed bytes objects for tag numbers 0–14, eliminating a bytes([...]) allocation on every call. These functions are called for every constructed type in every APDU.

  • Constructed type encode() methods use b"".join() – Converted all remaining bytes += concatenation in constructed.py encode methods (BACnetTimeStamp, BACnetCalendarEntry, BACnetSpecialEvent, BACnetObjectPropertyReference, BACnetRecipient, BACnetDestination, BACnetLogRecord, BACnetRecipientProcess, BACnetValueSource) and primitives.py helper functions (_encode_calendar_entry, _encode_special_event, _encode_recipient, _encode_cov_subscription) from O(n²) concatenation to O(n) b"".join().

  • PropertyIdentifier._missing_() vendor cache – Vendor-proprietary property IDs (512–4194303) are now cached so repeated lookups return the same pseudo-member instance instead of creating a new one each time.

  • encode_bit_string() / encode_character_string() pre-sized buffers – Replaced bytes([x]) + data two-object concatenation with pre-sized bytearray writes. decode_character_string() avoids an unnecessary bytes() wrapper when input is already bytes.

  • README broadened to cover all transports – Updated description, installation, examples table, Docker scenarios, and requirements to reflect BACnet/IP, IPv6, Ethernet, and Secure Connect support.

  • pyproject.toml cleanup – Broadened description, added bacnet-sc keyword, removed unused cli optional dependency, removed stale duplicate [tool.ruff] section (authoritative config is in ruff.toml).

[1.3.4] - 2026-02-14

Added

  • Self-signed certificate generation example (examples/sc_generate_certs.py) – Generates a test PKI (EC P-256 CA + hub and two node device certificates) and demonstrates TLS-secured BACnet/SC communication with mutual authentication by routing an NPDU between two nodes through a hub. Provides the missing guidance for users who need to test SC transport with real TLS instead of allow_plaintext=True.

  • Certificate generation guide – New “Generating Test Certificates” section in the BACnet Secure Connect documentation (docs/guide/secure-connect.rst) with step-by-step instructions for creating a self-signed CA and device certificates using EC P-256 and the cryptography library, including SAN configuration notes for IP address vs DNS hostname verification.

[1.3.3] - 2026-02-13

Added

  • BACnet/IP-to-SC gateway router example (examples/ip_to_sc_router.py) – Demonstrates bridging a BACnet/IP network and a BACnet Secure Connect network using NetworkRouter with dual transports (BIPTransport + SCTransport). Shows the real-world building modernisation pattern where existing IP controllers communicate transparently with new SC devices through a pure-forwarding gateway.

Fixed

  • audit_log.py example used invalid alias "al,1" – The short alias "al" is not registered in OBJECT_TYPE_ALIASES. Changed to "audit-log,1" which resolves correctly via hyphen-to-underscore conversion. Also fixed the matching code snippet in docs/guide/examples.rst.

[1.3.1] - 2026-02-13

Added

  • Docker integration tests for BACnet Secure Connect – New Scenario 9 (secure-connect profile) with real cross-container WebSocket communication between separate hub and node containers on Docker bridge networking. Three new container roles: sc-hub (SCHubFunction WebSocket server), sc-node1 and sc-node2 (SCTransport nodes with echo handlers). 9 test cases covering hub connection, unicast routing to each node, broadcast delivery to all nodes, bidirectional exchange, large NPDU transfer (~1400 bytes), rapid sequential messages (50 messages), and concurrent multi-node traffic.

  • make docker-test-sc Makefile target for running the SC Docker scenario independently; also added to the make docker-test chain.

  • 5 in-process SC integration tests moved from docker/scenarios/ to tests/transport/sc/test_sc_integration.py where they belong alongside the 224 existing SC unit tests. Total test count increased from 5,925 to 5,930.

Changed

  • Docker image now includes SC dependencies – Added --extra secure to both uv sync commands in docker/Dockerfile so websockets and cryptography are available inside containers.

[1.3.0] - 2026-02-13

Added

  • BACnet Secure Connect (Annex AB) – Full implementation of BACnet/SC per ASHRAE 135-2020 Annex AB, providing encrypted, authenticated BACnet communication over WebSocket/TLS with a hub-and-spoke topology and optional direct peer-to-peer connections. This is the largest feature addition since the initial release.

    New transport layer in src/bac_py/transport/sc/ (10 modules):

    • BVLC-SC codec (bvlc.py) – Encode/decode for all 13 BVLC-SC message types (BVLC-Result, Encapsulated-NPDU, Address-Resolution, Address-Resolution-ACK, Advertisement, Advertisement-Solicitation, Connect-Request, Connect-Accept, Disconnect-Request, Disconnect-ACK, Heartbeat-Request, Heartbeat-ACK, Proprietary-Message) with typed payload dataclasses, header option chaining, and control flag handling.

    • VMAC addressing (vmac.py) – 6-byte virtual MAC addresses with locally-administered unicast bit management and 16-byte RFC 4122 Device UUIDs for collision detection.

    • WebSocket I/O layer (websocket.py) – Sans-I/O websockets library integration with asyncio TCP/TLS streams for both client and server connections, binary frame send/receive, and subprotocol negotiation (hub.bsc.bacnet.org, dc.bsc.bacnet.org).

    • TLS context builder (tls.py) – TLS 1.3 client and server SSL context creation with mutual authentication, certificate chain loading, and plaintext mode for testing.

    • Connection state machine (connection.py) – Initiating peer (Figure AB-11) and accepting peer (Figure AB-12) state machines with Connect-Request/Accept handshake, periodic heartbeat (AB.6.3), graceful Disconnect-Request/ACK exchange, VMAC collision detection, and configurable timeouts.

    • Hub Function (hub_function.py) – WebSocket server (AB.5.3) that accepts hub connections from SC nodes, maintains a connection table indexed by VMAC, routes unicast messages to destination VMAC, replicates broadcasts to all connected nodes except the source, and detects VMAC/UUID collisions.

    • Hub Connector (hub_connector.py) – WebSocket client (AB.5.2) with persistent connection to a primary hub, automatic reconnection with exponential backoff (configurable min/max delay), and failover to a secondary hub when the primary is unavailable.

    • Node Switch (node_switch.py) – Direct peer-to-peer connection manager (AB.4) that listens for inbound direct connections, initiates outbound connections via address resolution through the hub, and maintains a connection pool with configurable limits.

    • SC Transport (__init__.py) – SCTransport class implementing the TransportPort protocol, wiring together the hub connector, optional hub function, and optional node switch. Integrates with the existing NetworkLayer transparently – the network layer sees standard 6-byte MAC addresses (VMACs) and uses the same send_unicast/send_broadcast API.

    • Types and constants (types.py) – BvlcSCFunction (13 message types), SCControlFlag, SCResultCode, SCHubConnectionStatus, header option types, WebSocket subprotocol names, and VMAC constants.

    Install with optional dependencies: pip install bac-py[secure] (websockets>=14.0, cryptography>=42.0).

  • Top-level lazy exportsSCTransport and SCTransportConfig are available from bac_py via __getattr__ lazy loading, so importing the package incurs no cost when SC is not used.

  • Docker SC integration scenariodocker/scenarios/test_secure_connect.py with 5 tests: two-node unicast via hub, broadcast to all nodes, hub failover, direct P2P connection, and concurrent message stress test.

  • 224 new unit tests across 10 test files covering BVLC-SC codec round-trips, VMAC generation and parsing, WebSocket client/server handshake, TLS context creation, connection state machine lifecycle, hub function routing, hub connector failover, node switch direct connections, address resolution, SC transport protocol compliance, and end-to-end NPDU exchange. Total test count increased from 5,701 to 5,925.

Documentation

  • BACnet/SC user guide – New docs/guide/secure-connect.rst covering hub connection, hub function setup, direct connections via Node Switch, TLS certificate configuration, failover, and VMAC address resolution.

  • BACnet/SC features page – Added comprehensive SC section to docs/features.rst with feature bullet list and code example.

  • API reference – Added all 8 SC sub-modules to docs/api/transport.rst (BVLC codec, VMAC, Connection, Hub Function, Hub Connector, Node Switch, WebSocket, TLS, Types).

  • Getting started – Added pip install bac-py[secure] installation instructions and optional dependency note.

  • Example scripts – Added examples/secure_connect.py (SC client with manual NPDU/APDU construction) and examples/secure_connect_hub.py (SC hub server with ObjectDatabase, Node Switch, and signal-based shutdown).

  • Examples guide – Added BACnet Secure Connect section to docs/guide/examples.rst documenting both new example scripts.

  • README – Added SC feature bullet, pip install bac-py[secure] installation section, updated architecture and test count.

[1.2.2] - 2026-02-13

Added

  • Test coverage improvements – Added ~640 new unit tests covering encoding edge cases, network layer validation, event engine branches, application lifecycle, server handler error paths, object model operations, transport frame validation, conformance PICS generation, segmentation accounting, service decode branch partials, and type system optional-field paths. Coverage improved from 95% (472 uncovered lines) to 99% (15 uncovered lines). Total test count increased from 5,061 to 5,701.

Fixed

  • Protocol stub coverage exclusions – Added # pragma: no cover to TransportPort, Serializer, and NetworkSender protocol class stubs (transport/port.py, serialization/__init__.py, network/__init__.py) since abstract method bodies (...) are untestable by design.

[1.2.1] - 2026-02-12

Fixed

  • read_bdt()/read_fdt() NAK handling – These client methods now convert the internal BvlcNakError to RuntimeError when the target device rejects the request (not a BBMD), matching the pattern already used by write_bdt(). Previously the internal exception type was not exported, making it impossible for callers to catch cleanly.

Changed

  • Targeted unicast discovery early returnwho_is() and discover() now auto-infer expected_count=1 when low_limit == high_limit and the destination is a unicast address. This avoids waiting the full broadcast timeout (typically 3s) when only one response is expected, reducing targeted discovery from ~3s to RTT (~50ms).

  • who_is_router_to_network() early return – Added expected_count parameter to who_is_router_to_network() (both BACnetClient and Client wrapper). When set, the method returns as soon as the expected number of distinct routers have responded instead of waiting the full timeout.

[1.2.0] - 2026-02-12

Breaking Changes

  • PropertyIdentifier enum values corrected – ~40+ numerical values realigned to match ASHRAE 135-2020 Clause 21 pp. 933-942. Key changes: TIME_DELAY_NORMAL 204→356, TIME_SYNCHRONIZATION_INTERVAL 205→204, lift/escalator properties renumbered (CAR_ASSIGNED_DIRECTION 500→448, etc.), staging properties renumbered (PRESENT_STAGE at 493, etc.), audit properties renumbered (AUDIT_LEVEL 550→498, etc.). New properties added: ISSUE_CONFIRMED_NOTIFICATIONS (51), INTERFACE_VALUE (387), LOW_DIFF_LIMIT (390), STRIKE_COUNT (391), and others.

  • EngineeringUnits enum expanded from 62 to 269 members – Complete per ASHRAE 135-2020 Clause 21. Many existing values corrected: WATTS 48→47, KILOWATTS 49→48, MEGAWATTS 50→49, LITERS_PER_SECOND 85→87, CUBIC_METERS 46→80, KILOGRAMS 28→39, and others.

  • StagingObject property names corrected – Renamed to match Clause 12.62: STAGING_STATEPRESENT_STAGE, TARGET_OBJECTSTAGES, TARGET_PROPERTYSTAGE_NAMES, STAGING_TIMEOUTTARGET_REFERENCES.

  • AccessEvent enum corrected – Renamed NO_ENTRY_AFTER_GRANTNO_ENTRY_AFTER_GRANTED, DENIED_INCORRECT_AUTHENTICATIONDENIED_INCORRECT_AUTHENTICATION_FACTOR, DENIED_OTHER 133→164, and 30+ new denied event members added.

Added

  • 5 new enumsAccessCredentialDisableReason (10 members), LiftCarDoorCommand (3), LiftCarDriveStatus (10), LiftCarMode (14), LiftFault (17) per ASHRAE 135-2020.

  • Bvlc6ResultCode fixVIRTUAL_ADDRESS_RESOLUTION_NAK corrected from 0x0060 to 0x0040 per Clause 7.

  • Examples guide – New docs/guide/examples.rst covering all 17 example scripts organized into 5 categories with code snippets and cross-references.

  • Device management guide sections – Added Device Communication Control, Reinitialization, Time Synchronization, and Object Management sections to docs/guide/device-management.rst.

  • 120+ new enum tests covering LiftFault, LiftCarMode, LiftCarDoorCommand, LiftCarDriveStatus, AccessCredentialDisableReason, AccessEvent, StagingState, EscalatorMode, EscalatorFault, LiftCarDirection, LiftGroupMode, LiftCarDoorStatus, AccessCredentialDisable.

Fixed

  • Docker infrastructure – Pinned uv from latest to 0.9 in Dockerfile for deterministic builds. Updated firmware/application version strings from "0.1.0" to "1.2.0" across all 4 server roles. Added BROADCAST_ADDRESS env var support to thermostat demo. Added .dockerignore exclusions (.github/, .env*, tests/, *.md).

  • Makefile CI alignment – Added docker/ to lint, fix, and format targets to match CI. Added --profile demo and --profile stress-runner to docker-clean target.

Changed

  • Documentation completeness – Added missing services (SubscribeCOVProperty, SubscribeCOVPropertyMultiple, ConfirmedCOVNotification, ConfirmedCOVNotificationMultiple, GetEventInformation, UnconfirmedCOVNotificationMultiple) to features.rst and README.md. Updated test count from 4,920+ to 5,050+. Fixed stale architecture link in features.rst.

[1.1.1] - 2026-02-12

Added

  • 5 new example scriptsdevice_control.py (communication control, reinitialization, time synchronization), object_management.py (create, list, and delete objects), advanced_discovery.py (Who-Has, unconfigured device discovery, hierarchy traversal), cov_property.py (property-level COV subscriptions with increment), audit_log.py (audit log queries with pagination). Total example count is now 17.

  • get_enrollment_summary() example in alarm_management.py – demonstrates querying enrollment summaries with acknowledgment filtering.

Fixed

  • extended_discovery.py – changed dev.address to dev.address_str for consistency with all other example scripts.

Changed

  • Updated README examples table and documentation to reflect the 5 new example scripts.

  • Added get_enrollment_summary() to the events-alarms guide and features convenience API list.

[1.1.0] - 2026-02-12

Added

  • 4 new Client wrapper methodstraverse_hierarchy() walks Structured View object hierarchies with string addressing; subscribe_cov_property_multiple() batches property-level COV subscriptions in a single request; write_group() sends unconfirmed WriteGroup channel writes; discover_unconfigured() finds unconfigured devices via Who-Am-I (Clause 19.7).

  • UnconfiguredDevice top-level exportUnconfiguredDevice is now importable directly from bac_py.

  • ~40 new Client unit tests covering string address parsing, BACnetAddress pass-through, broadcast destination defaults, enum string parsing, and new wrapper delegation.

Changed

  • Consistent string support across all Client methods – 11 methods that previously required typed parameters now accept strings: time_synchronization and utc_time_synchronization (address), atomic_read_file and atomic_write_file (address, file identifier), confirmed_private_transfer and unconfirmed_private_transfer (address), add_list_element and remove_list_element (address, object identifier, property identifier), subscribe_cov and unsubscribe_cov (address, object identifier), who_has (object identifier). All parsing functions handle typed pass-through, so existing code using BACnetAddress / ObjectIdentifier / PropertyIdentifier continues to work unchanged.

  • Internal deduplication – Extracted _resolve_broadcast_destination() and _parse_enum() helpers to replace ~20 copy-pasted address parsing, broadcast resolution, and enum parsing blocks across the Client class. Moved parse_address to a top-level import.

  • Documentation updates – Added discover_unconfigured() and who_has() string usage to the Discovery and Networking guide. Added subscribe_cov_property_multiple() example to the COV section. Updated traverse_hierarchy example in Features to use string-friendly syntax. Expanded the Convenience API feature list with new methods and consistent string support note.

[1.0.2] - 2026-02-12

Fixed

  • MS/TP and non-IP address parsingparse_address() now supports "NETWORK:HEXMAC" format (e.g. "4352:01" for MS/TP devices behind routers), enabling the high-level Client to communicate with devices on remote non-IP data links discovered via BBMD or router forwarding.

  • Router path learning from routed APDUs – The network layer now learns router paths from the SNET/SADR fields of incoming routed APDUs. When a response arrives from a remote network, the transport-level sender is cached as the router for that network, enabling efficient unicast routing for subsequent requests instead of broadcasting.

  • BACnetAddress.__str__() round-trip completeness – Address string output for non-IP MACs (1-byte MS/TP, 2-byte ARCNET, etc.) now round-trips correctly through parse_address().

Changed

  • Documentation sidebar restructure – Split monolithic examples.rst (907 lines) into 5 topical guide pages under docs/guide/ (Reading and Writing Properties, Discovery and Networking, Events and Alarms, Server Mode, Device Management and Tools). Reorganized the Sphinx sidebar from 4 flat entries into 3 captioned sections (Getting Started, User Guide, API Reference) with 17 navigable entries. API modules are now listed directly in the top-level toctree instead of behind an intermediate landing page.

  • API reference sidebar overhaul – Split 3 monolithic API pages (Application, Services, Objects) into 12 focused sub-pages grouped by category. Application is now split into Client, Server, and Engines; Services into Property, Discovery, Events, and Management; Objects into Base, I/O, Scheduling, Monitoring, and Infrastructure. Removed :undoc-members: from all API documentation to reduce noise. Each sub-page now has a manageable right-hand table of contents instead of listing 20-30+ sections on a single page.

  • Hot-path performance optimizations for typical building monitoring scenarios (100+ devices, 25-40 points each):

    • parse_address() string inputs now cached via lru_cache (O(1) repeated lookups in polling loops).

    • _resolve_object_type() and _resolve_property_identifier() alias resolution now cached via lru_cache.

    • BIPAddress.encode() uses socket.inet_aton() + struct.pack() instead of string splitting.

    • encode_npdu() pre-allocates bytearray to estimated packet size.

    • COVManager.check_and_notify() and check_and_notify_property() use secondary dict indices for O(k) dispatch (k = subscriptions on the changed object) instead of O(N) full scan of all subscriptions.

    • ObjectDatabase.get_objects_of_type() uses a type index for O(1) lookup instead of O(N) full scan.

Added

  • 137 new unit tests covering Ethernet/MS/TP support and performance optimizations:

    • NPDU variable MAC length encode/decode (1–8 byte SADR/DADR)

    • Mixed data link router forwarding (BIP↔MS/TP, 2-byte MAC, broadcasts)

    • Address str()parse_address() round-trips for all MAC formats

    • Network layer remote send with variable-length MACs and router cache learning

    • COV secondary index maintenance (subscribe/unsubscribe/expiry/shutdown)

    • ObjectDatabase type index (add/remove/query by type)

[1.0.0] - 2026-02-12

Added

  • Core protocol stack – Full BACnet/IP (Annex J) over UDP, ASN.1/BER encoding for all 13 primitive application tags, context-tagged constructed types, NPDU network layer with all 12 network messages, automatic segmented request/response handling (Clause 5.2), and Transaction State Machine with retry/timeout management.

  • All confirmed and unconfirmed services – ReadProperty, WriteProperty, ReadPropertyMultiple, WritePropertyMultiple, ReadRange, CreateObject, DeleteObject, AddListElement, RemoveListElement, AtomicReadFile, AtomicWriteFile, SubscribeCOV, SubscribeCOVProperty, EventNotification, AcknowledgeAlarm, GetAlarmSummary, GetEnrollmentSummary, GetEventInformation, ConfirmedTextMessage, DeviceCommunicationControl, ReinitializeDevice, ConfirmedPrivateTransfer, ConfirmedAuditNotification, AuditLogQuery, VT-Open/Close/Data, Who-Is/I-Am, Who-Has/I-Have, Who-Am-I/You-Are, TimeSynchronization, UnconfirmedCOVNotification, UnconfirmedEventNotification, UnconfirmedTextMessage, UnconfirmedAuditNotification, WriteGroup, and UnconfirmedPrivateTransfer.

  • 62 object types – Device, Analog/Binary/MultiState I/O/Value, Accumulator, Averaging, Calendar, Channel, Command, Event Enrollment, Event Log, File, Global Group, Group, Life Safety Point/Zone, Load Control, Loop, Network Port, Notification Class, Notification Forwarder, Program, Pulse Converter, Schedule, Staging, Structured View, Timer, Trend Log, Trend Log Multiple, Audit Reporter, Audit Log, Alert Enrollment, Access Door/Point/Zone/User/Rights/Credential, Credential Data Input, Elevator Group, Lift, Escalator, Lighting Output, Binary Lighting Output, and 12 generic value types.

  • Event engine – All 18 standard event algorithms (change-of-bitstring, change-of-state, change-of-value, out-of-range, floating-limit, change-of-life-safety, change-of-discrete-value, etc.), intrinsic reporting for 9 object types, NotificationClass recipient list routing with day/time filtering and per-recipient confirmed/unconfirmed delivery.

  • Schedule engine – Weekly and exception schedules with calendar-aware evaluation, wildcard dates, week-n-day patterns, and priority-based resolution writing to target object property references.

  • Trend log engine – Polled, COV-based (Clause 12.25.13), and triggered acquisition modes with configurable circular buffer management and property-change callbacks.

  • Audit logging – AuditManager with automatic audit records for write/create/delete operations, AuditReporter and AuditLog objects with buffer management, ConfirmedAuditNotification/UnconfirmedAuditNotification services, and AuditLogQuery for retrieval.

  • COV manager – Subscription lifecycle management, increment threshold enforcement, property-level and object-level subscriptions, confirmed and unconfirmed notification delivery.

  • High-level Client API – Simplified async context manager with string-based addressing, short aliases (ai, ao, av, bi, bo, bv, msv, pv, name, desc, etc.), auto-encoding/decoding, and convenience wrappers for discover, read, write, read_multiple, write_multiple, subscribe_cov, get_alarm_summary, get_event_information, acknowledge_alarm, send_text_message, backup, restore, query_audit_log, subscribe_cov_property, create_object, delete_object, device_communication_control, and reinitialize_device.

  • Server handlers – DefaultServerHandlers for ReadProperty, WriteProperty, ReadPropertyMultiple, WritePropertyMultiple, ReadRange, Who-Is, COV subscriptions, CreateObject, DeleteObject, device management, file access, and audit instrumentation.

  • Network routing – Multi-port BACnet router with dynamic routing tables (Clause 6), Who-Is-Router-To-Network discovery, and cross-network message forwarding.

  • BBMD – Broadcast Management Device with foreign device registration, BDT/FDT table management, cross-subnet forwarding, and IPv4 multicast (Annex J.8).

  • BACnet Ethernet – Raw IEEE 802.3 transport with 802.2 LLC headers (DSAP/SSAP=0x82) per Clause 7, Linux AF_PACKET and macOS BPF support.

  • BACnet/IPv6 – Full Annex U transport with 3-byte VMAC addressing, IPv6 multicast, address resolution with TTL caching, and foreign device registration.

  • Device info caching – Automatic caching of peer device capabilities from I-Am responses (Clause 19.4) for correct APDU size negotiation.

  • JSON serializationto_dict()/from_dict() on all data types with optional orjson backend, time series export/import (Annex AA) in JSON and CSV formats.

  • Conformance and PICS generation – PICSGenerator with full object introspection, BIBBMatrix with 40+ BIBB definitions and auto-detection.

  • Docker integration tests – 8 scenarios (Client/Server, BBMD, Router, Stress, Device Management, COV Advanced, Events, Demo) with real UDP communication between containers using Docker Compose with isolated bridge networks.

  • 12 example scripts – read_value, write_value, read_multiple, write_multiple, discover_devices, extended_discovery, monitor_cov, alarm_management, text_message, backup_restore, router_discovery, and foreign_device.