OpenRBus
OpenRBus 0.4.0 is an experimental, vendor-independent Python protocol core for reading CANopen-style objects from compatible BDR Thermea heating systems. It provides validated BLE segmentation, CAN-IP object access, a normalized public register registry, and an asynchronous high-level client.
The project is alpha software. Read support has hardware evidence for the documented BLE/CAN-IP path. Writes remain deliberately conservative: every currently published writable definition is classified as unverified, disabled by default, and requires an explicit unsafe opt-in. Do not use writes on a production heating system unless you understand and can recover from the possible effects.
Scope and boundaries
- The package contains no default PIN, shared credential, manufacturer key, or implicit secret path. Pairing and node authorization remain owner-controlled.
- The active BLE path carries CAN-IP messages behind a transparent-service selector. The separate RUB codec is available for offline and future-adapter work, but is not part of that active path.
- The registry contains normalized technical facts and short German/English labels. It contains no vendor binaries, decompiled source, packet captures, credentials, serial numbers, device addresses, or sampled runtime values.
- Compatibility varies with controller family, firmware, and configuration. Registry evidence is not a promise that an object exists on a particular installation.
- The public catalog projects registry metadata only for capabilities observed on a specific node; it never exposes the complete static registry as runtime device state.
Requirements and installation
OpenRBus requires Python 3.11 or newer.
python -m venv .venv
. .venv/bin/activate
python -m pip install -e '.[dev,ble]'
Omit ble when using only codecs, the registry, or a custom object-access
adapter.
For a released version, install the pinned package instead of an editable checkout:
python -m pip install 'openrbus[ble]==0.4.0'
Reading an object
The operating system must already be paired with the gateway. Some devices may also need an owner-supplied authorization callback.
import asyncio
from openrbus import ObjectAddress, OpenRBusClient, RawObjectClient
from openrbus.transport import BleakMessageTransport, ManagedTransport
async def main() -> None:
transport = ManagedTransport(BleakMessageTransport("platform-device-identifier"))
await transport.connect()
try:
raw = RawObjectClient(transport)
client = OpenRBusClient(raw)
result = await client.read(
1,
ObjectAddress(0x3654, 1),
device_family="Ehc-16",
)
print(result.definition.name("en"), result.value)
finally:
await transport.disconnect()
asyncio.run(main())
BLE identifiers are platform-specific. discover_ble_devices() can return
advertisements exposing the supported transparent service.
Reading multiple objects
read_many() coalesces same-node objects into hardware-validated CAN-IP
GetList requests when the object-access adapter supports that capability.
Requests are split at 100 objects and before declared value lengths would
exceed the validated gateway message limit. Oversized individual values use a
normal single-object read. Adapters without bulk support fall back to
sequential reads.
from openrbus import ReadFailure
results = await client.read_many(
4,
("3402:01", "340c:01"),
device_family="Scb-10",
)
for result in results:
if isinstance(result, ReadFailure):
print(result.address, result.error)
else:
print(result.address, result.value)
Results retain input order and carry per-object aborts without hiding successful values from the same batch.
Write safety
The default client permits only level-1 reads with complete access evidence.
Level-2/3 reads and all writes are blocked unless independently enabled. See
docs/access-policy.md for direct, file, and explicit
environment-variable configuration.
Writes have five independent gate groups:
- The client's access policy permits the register's declared level.
- Construct
OpenRBusClientwithenable_writes=True. - Pass
allow_unsafe=Truefor definitions that are not hardware-validated. - For registers with device evidence, satisfy the declared access level. An
active write verifies the authoritative effective level from
4002:00. - Pass a value satisfying the registry type, range, precision, family, and conflict checks.
Use dry_run=True first. A dry run validates and encodes the value without I/O,
but intentionally still requires both opt-ins.
from decimal import Decimal
client = OpenRBusClient(raw, enable_writes=True, max_access_level=2)
plan = await client.write(
1,
"2300:00",
Decimal("12.34"),
allow_unsafe=True,
dry_run=True,
)
print(plan.raw_value.hex())
print(plan.required_access_level, plan.higher_risk_access)
An active confirmed write is rate-limited and read back by default. Writes are always individual and sequential; OpenRBus provides no transactional or batch write. This does not make an unverified register safe; read-back only detects a byte-level mismatch.
Access levels 1, 2, and 3 mean user, installer, and professional. Level 2 and
above is explicitly higher-risk metadata. Access requirements are only present
where device-family evidence exists; unknown requirements are not invented.
See docs/writing.md for coverage and distinct access,
value, and write-safety errors.
Level-2/3 access is functionally equivalent to the installer/professional access available at the physical control panel with the documented code the documented owner-supplied installer code (see the public product manual). OpenRBus's TEA authorization does not provide a capability that physical access to the appliance plus that code would not provide. In both cases the protection against unauthorized access is physical: deny access to the heating room and appliance. Without physical access, the installation-specific BLE pairing PIN cannot be obtained, so OpenRBus cannot establish remote BLE access.
Warning: Incorrect level-2/3 settings can increase energy consumption, accelerate equipment wear, and reduce comfort. This is the same operational risk as using the physical installer/professional menu, not an additional security risk in the narrower access-control sense.
Optional node authorization
Level-1 reads and gated writes need no manufacturer key. Level-2/3 objects
remain blocked unless the node reports a sufficient effective level at
4002:00.
NodeAuthorizer implements the explicit, per-node challenge-response pipeline
for adapters that implement AsyncNodeAuthorizationAccess. It requires a
four-byte manufacturer-owned TEA key component which OpenRBus does not include,
derive, cache, or log. Users must obtain any required material themselves;
OpenRBus deliberately does not prescribe a source.
Supply the component directly for one call. The independent access policy must also permit the requested level:
await NodeAuthorizer(access, max_access_level=3).authorize(
4,
AccessLevel.PROFESSIONAL,
key_component=application_supplied_four_bytes,
)
Or supply an explicit external file path. The file contains the uint32 key component as four big-endian bytes or eight conventional hexadecimal digits:
await NodeAuthorizer(access, max_access_level=3).authorize(
4,
AccessLevel.PROFESSIONAL,
key_path="/path/outside/the/repository/node-authorization.key",
)
key_path_env="APPLICATION_SELECTED_VARIABLE" is also supported; the named
environment variable contains the file path, not the key. There is no default
path or variable name. The ordinary generic-purpose RawObjectClient does not
claim authorization-channel support; an adapter must explicitly implement the
channel switch required after the free-channel request.
On the active BLE/CAN-IP path, use the separately validated purpose-2 gateway
exchange before constructing the object client. The gateway confirmation is
not itself a node grant; refresh_session_access() must still prove the target
node's effective level from 4002:00:
from openrbus import CanIpGatewayAuthorizer, OpenRBusClient, RawObjectClient
await CanIpGatewayAuthorizer(transport, max_access_level=3).authorize(
3,
key_path="/explicit/path/outside/the/repository/node-authorization.key",
)
client = OpenRBusClient(
RawObjectClient(transport),
enable_writes=True,
max_access_level=3,
)
access = await client.refresh_session_access(4)
assert int(access.effective_level) >= 3
The gateway authorizer accepts the same three explicit key-source forms and also has no implicit path, variable name, cache, or secret-bearing log output.
Registry
The canonical dataset is data/registry/registry-v1.json.
It currently contains 3,066 canonical definitions and 47 explicitly separated
legacy candidates. The packaged compact copy is loaded with:
from openrbus import Registry
registry = Registry.load_default()
definition = registry.get("2300:00")
print(definition.name("de"), definition.wire.type, definition.wire.unit)
The language-neutral catalog and separate short DE/EN locale files are loaded
as one typed registry. The source code is Apache-2.0 licensed. The normalized registry dataset is
licensed under CC BY 4.0; see data/registry/README.md
for attribution and provenance boundaries.
Development
ruff check .
ruff format --check .
mypy src
pytest
python tools/check_publication.py
python -m build
Security issues and contribution rules are documented in SECURITY.md and
CONTRIBUTING.md. Protocol confidence levels and write limitations are
documented under docs/.
Disclaimer
OpenRBus is an independent interoperability project and is not affiliated with
or endorsed by BDR Thermea or its brands. Heating-system changes can cause
equipment damage, unsafe operation, loss of comfort, or loss of warranty. See
DISCLAIMER.md before connecting to real equipment.
Release files for openrbus 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| openrbus-0.4.0.tar.gz | 451.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| openrbus-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 688.4 kB
Release files / openrbus-0.4.0.tar.gz
| Download URL | openrbus-0.4.0.tar.gz |
|---|---|
| Size | 451.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8923f6afc787291fac41ba0b36367bdbe9b1b93d38040d89e1bf8a6614f52579
|
|
BLAKE2b-256 checksum How to use checksums |
2945a799cf8564a87094acb9ba73e26b252be7df47adc9827062e2ec586aee5b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / openrbus-0.4.0-py3-none-any.whl
| Download URL | openrbus-0.4.0-py3-none-any.whl |
|---|---|
| Size | 236.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ecd33244627757354a3f748ce8937fdb7bbb79de8e863ff80beb147af7d12db9
|
|
BLAKE2b-256 checksum How to use checksums |
2b75e8e97aff09ba2aa0c82edef46b938d7fccf5f24082a12b2143f6b7031f07
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency log