aioebus
A standalone, async, pure-Python eBUS library — the goal is to talk to an eBUS adapter (enhanced protocol) and decode Vaillant/eBUS messages without a separate ebusd daemon, so a Home Assistant integration can embed it directly (lib in PyPI, thin integration in HA Core).
This is an incremental port of ebusd's behaviour to Python — not a line-by-line transpile. Each layer is ported by spec and checked against real captured telegrams (a conformance harness), so the long tail of real-world quirks is caught automatically.
Status
| Phase | Scope | State |
|---|---|---|
| 0 | Scaffold + conformance harness (real captures as golden data) | ✅ |
| 1 | Datatype codecs (datatype.cpp port) + CSV type specs |
✅ |
| 2 | Message model + CSV ingest + decode (default rows, IGN, value tables) | ✅ |
| 2.5 | Full datatypes (date/time/float/bit), conditions as metadata | ✅ |
| 3 | eBUS frame/CRC + passive decode (wire bytes → named values) | ✅ |
| 3b | Enhanced-adapter transport (protocol ✅; live socket needs a window) | ◐ |
| 4 | Active read: build + scheduler + send FSM ✅ (offline); live send, scan, write | ◐ |
| 5 | Thin HA integration swaps its ebusd-HTTP client for this lib | ⏳ |
The enhanced adapter does bus arbitration in hardware, so the timing-critical part is offloaded — Phase 3 ports a documented host↔adapter byte protocol, not microsecond timing.
Phase 1 — what works now
Pure, offline decoding of eBUS field types, validated against telegrams captured from a real Vaillant cascade (2× aroTHERM, sensoCOMFORT, eloBlock, VR 71):
from aioebus import decode
decode("D2C", "bd01") # 27.8125 (flow temp)
decode("UCH", "0f") # 15
decode("STR:5", "4e45545833") # "NETX3"
decode("HEX:6", "090264186418") # "09 02 64 18 64 18"
decode("SIN", "f401", divisor=10) # 50.0 (CSV divisor column)
decode("UCH", "ff") # None (invalid/replacement sentinel)
Supported types: the integers (UCH SCH UIN SIN ULG U2L U4L, reversed UIR SIR), fixed-point D1B D1C D2B D2C FLT, floats EXP/EXR, BCD[:n] PIN, HEX:n, STR:n/STR:*, IGN:n, bit fields BI0..BI7[:n], and the date/time family (TTM TTH TTQ, HTM BTM VTM, HTI BTI VTI, HDA BDA[:3], BDY HDY, DTM DAY) -- with the CSV divisor column and ebusd's replacement (invalid) handling. This resolves every field type used across the 104 stock Vaillant configs.
Phase 2 — structured messages + CSV ingest
The in-memory model is structured dataclasses (MessageDef / FieldDef), never
CSV-shaped. The community ebusd-CSV library is read into the model with Python's
csv module (so quoted commas no longer abort a file). Our own curated
definitions can instead live in the native JSON format (convert.py) — both
become the same objects.
from aioebus.messages import parse_csv
msgs = {m.name: m for m in parse_csv(open("38.v32.csv").read())}
msgs["Statenumber"].decode_labeled(slave="1f") # {'Statenumber': 'sommer'}
msgs["PositionValveSet"].decode_labeled(slave="64") # {'PositionValveSet': 'dhw'}
tools/demo_live.py <csv> <telegrams.db> parses a real config and decodes the
newest live telegram for every message — pure Python, no ebusd decoding.
python -m aioebus.convert <csv> [out.json] converts curated CSVs to the native
structured format.
Phase 3 — frame layer + passive decode
The wire format is parsed end to end: escaping (A9 01→AA, A9 00→A9), the
eBUS CRC-8 (polynomial 0x9B, computed over the escaped stream), and the full
frame structure incl. NAK-retransmission. Combined with the message model this
turns raw bytes seen on the bus into named values, read-only:
from aioebus import parse_csv, MessageRegistry, decode_raw
reg = MessageRegistry(parse_csv(open("38.v32.csv").read()))
decode_raw(reg, "3138b509030d1800740003910100bb00") # (FlowTemp def, {'FlowTemp': 25.0625})
Validated bit-exact against 285 358 complete telegrams captured from the live
bus: every one parses, matches ebusd's own decoded columns and verifies CRC
(tools/validate_frame_db.py).
Phase 3b — enhanced-adapter transport
transport.py decodes the adapter's framed byte stream (11ccccdd 10dddddd
symbols, short form for bytes < 0x80), assembles telegrams on SYN and decodes
them — all read-only, nothing is ever written to the bus:
from aioebus import parse_csv, MessageRegistry, listen
reg = MessageRegistry(parse_csv(open("38.v32.csv").read()))
async for msgdef, values in listen("192.168.178.179", 9999, reg):
print(values)
The protocol logic is validated offline by round-tripping real telegrams through
a synthetic adapter stream, including arbitrary read fragmentation
(tests/test_transport.py). Live listening is gated on hardware: the enhanced
adapter is single-client, so ebusd must be stopped first (a maintenance window).
It has been run live against the reference adapter — passive decode works
end-to-end without ebusd.
Phase 4 — active reads under limited bandwidth
active.build_read() constructs the on-wire master request; its CRC matches
ebusd for every real read in the log. poll.PollScheduler decides what to read
and when, because eBUS is ~2400 baud and only a handful of reads fit per second:
- only
r1..r9are polled; the priority digit scales the interval (r1most frequent), plainris on-demand,w/unever; - passive-first — a value seen on the bus refreshes that register's timer, so broadcasts are never re-polled (on the live bus this is most of the traffic);
- one request at a time — the scheduler hands back only the most-overdue message.
The bus send FSM (active.ReadDriver/ActiveReader) is ported from ebusd's
protocol_direct.cpp: arbitrate (START with QQ) → send ZZ PB SB NN DD + CRC,
each byte echoed back and verified for collision → master ACK → read the
slave frame and check its CRC → ACK → SYN. The pure ReadDriver is driven in
tests by a simulated adapter through a full round-trip (arbitration-won, echoes,
answer, decode) plus the arbitration-lost and collision paths — all offline.
The live send (tools/active_read.py, reads only) uses master address 0x31,
the address ebusd itself uses on the reference bus: known-good, and free exactly
while ebusd is stopped (which the single-client adapter requires anyway). It has
not been run on hardware yet — that is the next maintenance window.
Native config format + converter
configs/vaillant/*.json are native-format device definitions (eloBlock, netx3)
— our own, independently-created definitions, Apache-2.0. aioebus.convert
turns any ebusd CSV into this format:
python -m aioebus.convert path/to/15.ctlv3.csv ctlv3.json
The ebusd-configuration CSVs are GPL-3.0, so a file converted from them is a
GPL-3.0 derivative: convert those locally, against your own copy — this
Apache-2.0 library ships only our own definitions, never a derivative of that
GPL data. tools/convert_configs.py regenerates our own set and the HA
translations base.
Names are not baked in: aioebus.translations.ha_strings() derives a stable
translation_key per field and emits configs/vaillant/translations/strings.json
(the English base); a de.json is a translated copy with the same keys.
Dev
python -m pytest # conformance tests (real captures)
python tools/validate_frame_db.py /path/to/telegrams.db # frames vs ebusd (100%)
python tools/validate_against_db.py /path/to/telegrams.db # optional field decode
Metadata
Release files for aioebus 0.1.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 | Provenance | |
|---|---|---|---|---|
| aioebus-0.1.0.tar.gz | 47.9 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aioebus-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 86.0 kB
Release files / aioebus-0.1.0.tar.gz
| Download URL | aioebus-0.1.0.tar.gz |
|---|---|
| Size | 47.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
022eae6bf85b6f752618558b3aa2edea5ea328004ec4fe7b4c9761c14fede18e
|
|
BLAKE2b-256 checksum How to use checksums |
f5f112232dc11b83bee593a2896ed05b0c4b93dd80868e4dd7497c028cdf8cfc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Oct 23, 2026.
Transparency logRelease files / aioebus-0.1.0-py3-none-any.whl
| Download URL | aioebus-0.1.0-py3-none-any.whl |
|---|---|
| Size | 38.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b2bd96630a8fd0958aa2889a10ec46a246bd41373d28c9b737739a490cdbeb82
|
|
BLAKE2b-256 checksum How to use checksums |
d1623c8e9eb8d8466d784431e89e210d19d42b1403dc1534a4f950c7bf1e8964
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Oct 23, 2026.
Transparency logSecurity for aioebus 0.1.0
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for aioebus-0.1.0.tar.gz
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 Oct 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.