Skip to main content

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..r9 are polled; the priority digit scales the interval (r1 most frequent), plain r is on-demand, w/u never;
  • 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.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aioebus 0.2.0
File Size Uploaded Provenance
aioebus-0.2.0.tar.gz 48.6 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for aioebus 0.2.0
File Interpreter ABI Platform
aioebus-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 87.3 kB

Release files / aioebus-0.2.0.tar.gz

Download URL aioebus-0.2.0.tar.gz
Size 48.6 kB
Tags Source
SHA-256 checksum
How to use checksums
9d005ea28d085d03b97636a2d0e0677fef4c45bcd5024a9e8e5b90cb2892f941
BLAKE2b-256 checksum
How to use checksums
15a15decadb9238aaa2f447e2d02bd86e4f40fa9f88ddb3447c54d0282b806b3
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 log

Release files / aioebus-0.2.0-py3-none-any.whl

Download URL aioebus-0.2.0-py3-none-any.whl
Size 38.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
763b470d880420378d112a6a44fa8d3465cd94d5232ebe4b45cfefa980679255
BLAKE2b-256 checksum
How to use checksums
8d9570653d9cab4ebadebca11e06a80fb525e243d1e58e8f69074af75972cdf7
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 log

Security for aioebus 0.2.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.

Select from 2 files

Attestation for aioebus-0.2.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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page