ace-bacnet-devices
Proprietary BACnet extensions, extracted from vendor PICS conformance statements and made
usable from bacpypes3.
Vendors declare their proprietary properties and object types in the PICS (Protocol Implementation Conformance Statement) they publish for each product. Those documents are public but unstructured — PDFs of hand-built tables, a different layout per vendor. This project reads them into a semantic model and turns that model into registered extension handlers, so a proprietary property decodes as a typed value instead of an opaque blob.
Using the extensions
pip install ace-bacnet-devices[bacpypes3]
import bacpypes_extensions.all # every bundled vendor
value = await app.read_property(address, ("device", 1), "serial-number")
Or register a single vendor:
import bacpypes_extensions.carrier
Both are plain imports with no configuration. The same thing as function calls, which is often clearer inside an application:
from bacpypes_extensions import load, load_all, loaded
load("carrier", "johnson-controls")
load_all()
loaded() # {16: 'Carrier', 5: 'Johnson Controls Inc.', ...}
Importing the bacpypes_extensions package itself does nothing. Python runs a package's
__init__ before any submodule, so a package that registered everything on import would
make import bacpypes_extensions.carrier register everything plus Carrier — the opposite
of asking for one vendor. Registration lives in the submodules, so what you import is what
you get. It is idempotent per vendor id.
What ships
| Definition | Vendor id(s) | Properties | Object types |
|---|---|---|---|
johnson-controls |
5 | 406 | 32 |
tac-micronet |
11 (also 10, 335) | 70 | 5 |
honeywell-international |
17 | 54 | 16 + 6 proprietary |
carrier |
16 | 29 | 5 |
automated-logic |
24 | 29 | 5 |
oemctrl |
446 | 29 | 5 |
ace-bacnet vendor list # what's bundled
ace-bacnet vendor show johnson-controls # every property, with datatype and range
ace-bacnet vendor ids "Schneider Electric" # resolve a name to BACnet vendor ids
Extracting from your own PICS documents
Source PICS files are third-party vendor publications and are not distributed with this project. Point the extractor at a local folder of them:
ace-bacnet extract /path/to/pics/ # a folder, searched recursively
ace-bacnet extract mydevice.pdf -o extracted/ # one document
ace-bacnet extract /path/to/pics/ -o extracted/ --vendor-id 16
Each document becomes a VendorExtensions — the same shape as the bundled definitions — so
anything you extract loads exactly like the ones that ship:
from ace_bacnet_devices import install, vendors
install(vendors.load_file("extracted/mydevice.json"))
To run the extraction-agreement tests, set ACE_BACNET_PICS_DIR to a folder holding
F-27461-2.pdf (the TAC MNB-1000 statement). Without it those tests skip.
How it works, and why it is careful
The datatype a vendor declares is the wire type
PICS tables routinely declare a property Unsigned and then describe an enumeration in
prose — BBMD_Mode is "0 – IP node only, 1 – BBMD mode, 2 – foreign device". Emitting
that as a BACnet Enumerated would move the application tag from 2 to 9 and silently
mis-decode every read from a real device.
So every generated datatype subclasses the primitive the PICS declared. Encoding is inherited unchanged — there is a byte-level parity test against the base primitive for each one — and the subclass adds:
- range limits, enforced on construction and on decode, so an out-of-range response from a device fails loudly;
- length limits, since neither stack constrains
OctetString/CharacterStringand the documents state "8 octets" / "20 characters max."; - documented value names, as inert metadata. Naming a value must never affect encoding.
Extraction never guesses
Anything uninterpretable becomes a typed issue rather than a wrong declaration:
- an unreadable datatype drops the property — a guessed primitive mis-decodes traffic, which is worse than a missing handler;
- constructed types (
Complex,Structure,Object Reference) are refused explicitly: there is no primitive to subclass, so no handler can exist; - a value legend needs two distinct entries before it is emitted;
"1, 4, or 6 octets"yields no length, because enforcing one would reject valid values;- an
ExtractionResultholds no vendor id. A document can parse perfectly and still not be installable, because the vendor id is not in it —.build(vendor_id=…)is a separate, explicit step.
Four document layouts
The corpus is not one format. Three table layouts and one text layout are read, and they differ in where the proprietary marker lives:
GROUPED | Object Type | Proprietary Properties: Name | ID | Datatype | Access | Range |
FLAGGED | Property Name | Required | Optional | Proprietary | Property ID | Data Type |
COMBINED | Property name | R/O/P | Supported | Property ID, Range, Data Type |
OUTLINE 5. Proprietary properties:
4145 active-locale-index unsigned
A fifth shape — a single cell listing property names with no identifier and no datatype — cannot be read by any parser, because the document simply does not contain what a handler needs.
Vendor ids, consolidation, and ambiguity
Proprietary numbers are scoped per BACnet vendor id, but a PICS names its author only in
prose. The id comes from ASHRAE's registry, bundled here as a snapshot (vendor-ids.json,
1638 entries).
Lookup deliberately refuses to guess between candidates, because a wrong vendor id silently mis-scopes every property. Two mechanisms handle the fallout:
VendorGroup— ids that may carry the same declarations. Derived from the registry where one organisation holds several (Siemens Schweiz AGis 7, 9 and 22), and curated with evidence for acquisitions: Schneider / TAC is 10, 11, 335, because the MNB family spans a 2008 TAC-branded statement and a 2022 Schneider-branded one for the same controllers. Installing registers under all of them, so a fleet spanning an acquisition decodes whichever id its firmware reports.VendorOverride— a recorded human decision, with its reasoning, for a name the registry cannot disambiguate. "Carrier" matches three entries; 16 was chosen. The ambiguity stays visible viafind(name, use_overrides=False).
Same-name grouping matches on the exact registered name, never the fuzzy lookup key —
that key strips corporate suffixes, which would merge TAC (11) with TAC AB (19), a
different company sharing a trading name.
Merging a vendor's documents
A vendor publishes one PICS per product, so the same property recurs across many files.
VendorExtensions.merge_all() collapses them into one installable definition and reports
genuine disagreements rather than silently overwriting. Merging is where the model gets
stress-tested — 27 Johnson Controls documents disagree in hundreds of places:
- a datatype disagreement has no safe resolution, so the property is dropped by default
(
binaryValue.DEFAULT_VALUEisRealin one document andBooleanin another); - a name disagreement is cosmetic, since the identifier is what travels on the wire;
- PDF artifacts are folded rather than treated as conflicts — a line break truncates a name
(
continuous-→continuous-proportional, prefer the longer) and a footnote marker attaches to one (ITEM_REFERENCE5→ITEM_REFERENCE, prefer the shorter). What tells them apart is that a footnote adds only digits.
Registrability is enforced in one place used by both extraction and merging, because two documents can be unregisterable together while neither is alone.
The legacy bacpypes stack
Legacy bacpypes cannot be imported on Python 3.12+ — it depends on the removed asyncore
module. Its adapter therefore emits source rather than registering at runtime:
ace-bacnet vendor generate-legacy tac-micronet -o tac_legacy.py
The generated module is syntax-checked and its guard logic unit-tested against stub bases,
but it has not been exercised against a real bacpypes install — that needs an older
interpreter.
Development
uv sync --all-extras
uv run pytest
uv run ruff check . && uv run ruff format --check .
uv run pyrefly check
uv run pre-commit install
Regenerate derived files after changing the bundled definitions:
uv run python scripts/build_tac_mnb1000.py # the hand-transcribed test oracle
uv run python scripts/build_extension_modules.py # per-vendor import shims
License
MIT — see LICENSE.
The bundled vendor definitions are factual data extracted from publicly published BACnet conformance statements. The source documents themselves are the property of their respective vendors and are not distributed here.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ace_bacnet_devices-0.1.0.tar.gz.
File metadata
- Download URL: ace_bacnet_devices-0.1.0.tar.gz
- Upload date:
- Size: 191.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14bfa37face100a7c790f0e6670ae4d4c14952205a45599b844f06ad5348f67e
|
|
| MD5 |
1fe1c3700feafbee4a0699e4b5cb3031
|
|
| BLAKE2b-256 |
6d6c722aa9967926829781e6f1439a218ec1ffb6b029ee4230af9c9cdb37b654
|
Provenance
The following attestation bundles were made for ace_bacnet_devices-0.1.0.tar.gz:
Publisher:
pypi-publish.yml on ACE-IoT-Solutions/ace-bacnet-devices
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ace_bacnet_devices-0.1.0.tar.gz -
Subject digest:
14bfa37face100a7c790f0e6670ae4d4c14952205a45599b844f06ad5348f67e - Sigstore transparency entry: 2443689323
- Sigstore integration time:
-
Permalink:
ACE-IoT-Solutions/ace-bacnet-devices@53c4bfff555adbfdc96afa5af17f0da97b96d058 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ACE-IoT-Solutions
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@53c4bfff555adbfdc96afa5af17f0da97b96d058 -
Trigger Event:
release
-
Statement type:
File details
Details for the file ace_bacnet_devices-0.1.0-py3-none-any.whl.
File metadata
- Download URL: ace_bacnet_devices-0.1.0-py3-none-any.whl
- Upload date:
- Size: 125.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ac396acf3c47ad9e441e8a2c86f091dfbe5f8542ab005e91ce2e5f7c76808687
|
|
| MD5 |
0fab8db41aba6468c5c3eed908268034
|
|
| BLAKE2b-256 |
2e66952dd1bfb04eb3bdfb5c04f6294f1da93022250d327aad973ba770a2fa77
|
Provenance
The following attestation bundles were made for ace_bacnet_devices-0.1.0-py3-none-any.whl:
Publisher:
pypi-publish.yml on ACE-IoT-Solutions/ace-bacnet-devices
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ace_bacnet_devices-0.1.0-py3-none-any.whl -
Subject digest:
ac396acf3c47ad9e441e8a2c86f091dfbe5f8542ab005e91ce2e5f7c76808687 - Sigstore transparency entry: 2443690348
- Sigstore integration time:
-
Permalink:
ACE-IoT-Solutions/ace-bacnet-devices@53c4bfff555adbfdc96afa5af17f0da97b96d058 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ACE-IoT-Solutions
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@53c4bfff555adbfdc96afa5af17f0da97b96d058 -
Trigger Event:
release
-
Statement type: