This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
BACpypes3 is a BACnet application and network layer library for Python 3.8+, rewritten from bacpypes (Python 2.5+) around asyncio and async/await. Package name: bacpypes3. Version is single-sourced in bacpypes3/__init__.py (__version__) and picked up by hatch via pyproject.toml.
Install for development (choose one — the repo carries a Pipfile, pyproject.toml with a dev dependency group, and a uv.lock):
pip install -e ".[websockets,ifaddr,yaml,rdflib]" # editable install with optional extras
uv sync # or with uv: installs dev group per uv.lockRun tests (config in pytest.ini sets asyncio_mode = auto, so async tests need no decorator):
pytest # full suite
pytest tests/test_pdu # a subpackage
pytest tests/test_pdu/test_address.py # a file
pytest tests/test_pdu/test_address.py::TestLocalStation::test_local_station # a single test
pytest -k who_is # by keywordLint / format (declared in the dev group):
ruff check bacpypes3 tests
ruff format bacpypes3 testsRun the built-in interactive shell (Read/Write/Who-Is/RPM/BBMD ops against a real network):
python -m bacpypes3 --help
python -m bacpypes3 --address 192.168.1.10/24 --instance 999
python -m bacpypes3 -v # version + which optional deps are loadedBuild a release with uv build; release_to_pypi.sh and release_to_testpypi.sh wrap that plus uv publish --index … for the two PyPI targets. To cut a new release end-to-end (bump __version__, commit, tag v<version>, push, then hand off to release_to_pypi.sh), run ./release.sh <new-version> — e.g. ./release.sh 0.0.110.
BACpypes3 is layered as a stack of asyncio-based communication objects wired together with bind(). Understanding the wiring model is the prerequisite for everything else.
Every layer is one or more of:
Client[T]— sends downstream viarequest(), receives upstream viaconfirmation().Server[T]— receives downstream viaindication(), sends upstream viaresponse().ServiceAccessPoint/ApplicationServiceElement— the "SAP/ASE" pair used to expose a service across a peer boundary (e.g. BVLL, application service).
bind(*objs) stitches adjacent objects together (clientPeer / serverPeer, and SAP↔ASE via serviceElement / elementService). Objects can register with string IDs (cid / sid) so bind can find them later. ConfigurationError is raised for double-binds or unbound requests. When adding a new protocol layer, subclass Client and/or Server, override indication/confirmation, and bind it into the stack — do not call the peer directly.
- Link layer:
bacpypes3/ipv4/,bacpypes3/ipv6/,bacpypes3/vlan/,bacpypes3/sc/(BACnet/SC over websockets, only imported ifwebsocketsis installed).ipv4/link.pyprovidesNormalLinkLayer,ForeignLinkLayer,BBMDLinkLayer;ipv4/bvll.pydefines the BVLL PDUs;ipv4/service.pyhasBVLLServiceAccessPointandBVLLServiceElement. - Network layer:
npdu.py(NPDUs) andnetservice.py(NetworkServiceAccessPoint,NetworkAdapter, routing).app.pyowns annsapand oneNetworkAdapterper bound link layer. - Application layer:
apdu.py(APDUs, confirmed/unconfirmed service tables),appservice.py(ApplicationServiceAccessPoint— segmentation, transactions, timers), andapp.py(Application— the top-level object;Application.from_args(args)is the standard entry point and builds the whole stack from parsed CLI/settings). - Object system:
primitivedata.py→constructeddata.py→basetypes.py→object.pyform the BACnet type system (atomic types, sequences/arrays/choices, standard base types, thenObjectand standard object types).vendor.pyhandles vendor extensions and per-vendor object/property identifier parsing (used pervasively byapp.parse_object_property_reference/parse_property_reference). - Local objects:
bacpypes3/local/— concrete implementations you register with anApplicationto be a BACnet device (analog.py,binary.py,multistate.py,schedule.py,networkport.py,device.py,cov.py,event.py,fault.py,oos.py). - Application services:
bacpypes3/service/— reusable service mixins (cov.py,device.py,object.py) that plug intoApplication.
settings.py— globalsettingsdict merged from defaults, env, INI/JSON/YAML config, and CLI flags.argparse.SimpleArgumentParser(inbacpypes3/argparse.py) is the standard CLI entry: it wires logging, readsBACpypes.{ini,json,yml}, and pre-populates the argsApplication.from_argsconsumes.debugging.py—ModuleLogger,bacpypes_debugging,DebugContents. The idiom seen throughout the codebase:_debug = 0 _log = ModuleLogger(globals()) @bacpypes_debugging class Foo: _debug: Callable[..., None] def bar(self): if _debug: Foo._debug("bar %r", ...)
_debug = 0is a module-level toggle;--debug bacpypes3.<module>on the CLI flips it and routes_log/Foo._debugoutput. Follow this pattern in new modules rather than rawlogging.getLogger.console.py+cmd.py— line-oriented async shell used by__main__.pyand manysamples/.json/andrdf/— bidirectional serialization of BACnet objects (sequence_to_json,json_to_sequence;sequence_to_graphfor RDF). Used byApplication.from_argsto hydrate objects from config.mcp.py— Model Context Protocol integration (optionalmcpextra). Exposes each__main__CLI command as an async function returning JSON-serializable data; can also run as a stand-alone MCP server (stdio) or be embedded in a long-running application viamcp.serve_http(...)as a concurrent asyncio task. Reference embedding pattern:samples/mini-device-mcp.py.analysis.py— pcap decoding helpers (pylibpcapextra).
Layout mirrors the package (tests/test_pdu, tests/test_app, tests/test_vlan, …). tests/conftest.py calls setup_package/teardown_package from tests/utilities.py. tests/state_machine.py + tests/clocked_test.py provide a deterministic virtual clock and state-machine harness — use these instead of real time / real network when adding tests for stack behavior. test__template.py is a scaffold to copy from.
samples/ is a large, load-bearing part of the documentation — real programs demonstrating a specific stack shape (foreign device, BBMD, VLAN, multiple stacks, COV client/server, RDF/CSV discovery, Application.from_args variations, etc.). When implementing something non-obvious, check whether a samples/*.py file already demonstrates it before designing from scratch.
Several modules only import when their extra is present (websockets → bacpypes3.sc, rdflib → bacpypes3.rdf, ifaddr, PyYAML, python-libpcap). Match this pattern for new optional integrations: import inside a try/except ImportError in bacpypes3/__init__.py and gate the feature behind the same guard elsewhere.