Proxy a CTA-2045 (EcoPort) Smart Grid Device onto the Electrification Bus (eBus / Homie 5), via a pluggable UCM backend
Project description
cta2045-proxy
Proxy a CTA-2045 (EcoPort) Smart Grid Device onto the Electrification Bus (eBus / Homie 5), through a pluggable UCM backend.
Status: pre-alpha. The SkyCentrics UCM backend is functional and validated end to end against a live SkyCentrics Ethernet UCM: telemetry decode, the full Homie device tree, and bidirectional demand-response control. The public API may still change before 1.0. Published to PyPI as
cta2045-proxy(pip install cta2045-proxy). It has not been certified under any conformance program and carries no warranty of interoperability.
What it does
A CTA-2045 appliance (an SGD, e.g. a water heater) speaks demand-response through a UCM. This service bridges that appliance onto eBus so any eBus/Homie 5 controller can read its telemetry and command demand-response events, published per the eBus proxy and water-heater data models:
ebus/5/{MAC}/ energy.ebus.device.bridge (the UCM)
ebus/5/{MAC}-wh01/ energy.ebus.device.water-heater (the proxied appliance)
The UCM is a pluggable backend, not a fixed assumption. CTA-2045 standardizes only the SGD-to-UCM link; how a UCM reaches the network is vendor-specific, so each UCM is a separate backend behind the vendor-neutral cta2045.ucm.Ucm interface. A SkyCentrics Ethernet UCM (over MQTT) ships today; the same CTA-2045 decode and eBus device model serve any future UCM without change:
- other vendors' CTA-2045 UCMs (add a backend for that dongle's transport),
- a native own-UCM that drives the CTA-2045 RS485 link layer directly, with no vendor dongle at all (reserved; see below).
Adding support for a new UCM means writing one backend; the appliance-side mapping and Homie publishing are shared across all of them.
Architecture
Three layers, cleanly separated:
cta2045(upstream lib) - the CTA-2045 protocol codec and the abstract, vendor-neutralcta2045.ucm.Ucminterface. Pure, no I/O.cta2045_proxy.ucm- concrete UCM backends bindingUcmto a real transport. Variants differ ONLY here:skycentrics- a SkyCentrics Ethernet UCM (US08C) reachable over MQTT. Ships today.native- an own-UCM driving the CTA-2045 RS485 link layer directly, no vendor dongle. Reserved; blocked oncta2045.link.
cta2045_proxy.mapping+.emitter- the CTA-2045 -> eBus water-heater data-model translation and the Homie adapter, shared across all backends. Built on ebus-sdk's declarative proxy layer (PropertySpec+build_from_declarations); see the SDK'sdoc/building-a-proxy.md.
Install
pip install cta2045-proxy
Requires Python 3.10+. For local development, clone the repo and install editable with the dev extras instead: pip install -e ".[dev]".
Run
cp config/config.example.toml config/config.toml # then edit
export EBUS_MQTT_PASS=... # secrets via env, never the file
cta2045-proxy --config config/config.toml
See config/config.example.toml for the [ucm] backend selector and the [ebus] output broker. A downstream deployment customizes only the config; the code is deployment-neutral.
The command runs in the foreground and waits until interrupted (SIGINT/SIGTERM), tearing down its eBus tree on exit.
usage: cta2045-proxy [-h] --config CONFIG [--log-level LOG_LEVEL] [--version]
--config CONFIG path to the config TOML (required)
--log-level LOG_LEVEL logging level (default: INFO, or $LOG_LEVEL)
--version print the version and exit
-h, --help show this help and exit
Exit codes: 0 on clean shutdown, 2 on a usage error (missing or unknown flag), 1 on a startup or runtime error (bad config path, malformed TOML, or a failure in the run loop).
Bring up a SkyCentrics UCM
tools/ has helper scripts to discover a SkyCentrics Ethernet UCM on your LAN, point it at a broker, and confirm it is publishing (ucm-discover, ucm-configure, ucm-watch), plus a how-to for standing up a local test broker (via the sibling broker-quickstart or a bare Mosquitto). See tools/README.md.
Example: the published Homie tree
A live capture of what the proxy publishes for one SkyCentrics UCM (84FEDC538C72) feeding one water heater. The bridge is the UCM; its child is the proxied appliance:
ebus/5/
├── 84FEDC538C72/ # bridge device (the UCM)
│ ├── $state = ready
│ ├── info/
│ │ ├── vendor-name = SkyCentrics
│ │ └── firmware-version = v2.2 (26 June 2026, 9:0:0)
│ └── connection/
│ ├── feeds-device-id = 84FEDC538C72-wh01
│ ├── feeds-device-type = energy.ebus.device.water-heater
│ ├── feeds-device-status = OK
│ └── last-seen = 2026-07-05T20:24:49Z
└── 84FEDC538C72-wh01/ # proxied device (the water heater / SGD)
├── $state = ready
├── info/
│ └── fuel-type = (unset until the SGD reports it)
├── meter/
│ ├── active-power = 0.0 W
│ └── imported-energy = 733.0 Wh
├── soc/
│ ├── soe = 0.0 Wh
│ ├── total-energy-storage = 4500.0 Wh
│ └── loadup-headroom = (unset)
├── flex/
│ ├── response = NONE
│ ├── opt-out = NONE
│ ├── request = (settable — publish a flex command to .../flex/request/set)
│ └── active-request = {"mode": "NORMAL"}
└── status/
└── fault-state = OK
flex is the vendor-neutral demand-response control-and-feedback capability (energy.ebus.capability.flex). The settable request property advertises the device's accepted control surface in its Homie 5 $format JSONSchema, and the proxy validates each inbound /set against it (strictly: unknown fields are rejected, not ignored) before translating. For a CTA-2045 SGD that schema accepts mode (SHED / LOAD_UP / NORMAL), intensity (PEAK / EMERGENCY / ADVANCED), duration (seconds), an advisory cause, and, for load-up, a target-percentage (heat until stored-energy soc reaches that %, encoded as CTA-2045 Advanced Load Up). It omits level (the SGD sheds/loads-up without a percentage) and temporary-setpoint (CTA-2045 quantifies Advanced Load Up in watt-hours, not degrees).
Send a demand-response command by publishing to the settable request property, e.g. a 10-minute shed:
mosquitto_pub -t 'ebus/5/84FEDC538C72-wh01/flex/request/set' -m '{"mode":"SHED","duration":600}'
The proxy validates it against the request $format, encodes it to CTA-2045, and forwards it to the UCM (devices/84FEDC538C72/ctl/shed); the SGD's resulting state flows back to flex/response (e.g. CURTAILED).
Development
ruff check . && ruff format --check .
pytest tests/ -v
Optionally install the pre-commit hooks (ruff format + lint on every commit, mirroring CI):
pip install pre-commit && pre-commit install
CI (.github/workflows/) runs the same ruff checks and pytest on 3.10 / 3.12 for every push and PR.
Releasing
The version has a single source of truth: __version__ in src/cta2045_proxy/__init__.py. Both build paths read it (the modern pyproject.toml via dynamic = ["version"], and the legacy setup.py shim for Yocto/kirkstone), so there is exactly one line to bump.
To cut a release:
- Bump
__version__insrc/cta2045_proxy/__init__.py(the only place). - Commit it (
git commit -am "release X.Y.Z"). - Tag it to match, prefixed with
v:git tag vX.Y.Z. - Push the tag:
git push --tags(a plaingit pushalone does not trigger a release).
Pushing a v* tag runs publish.yml: it reruns the tests, verifies the tag equals v$__version__ (a mismatch fails the run before anything is published), builds the sdist + wheel, and publishes to PyPI via Trusted Publishing (OIDC, no stored token). The tag and the packaged version can therefore never disagree in a published artifact.
References
Project details
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 cta2045_proxy-0.2.0.tar.gz.
File metadata
- Download URL: cta2045_proxy-0.2.0.tar.gz
- Upload date:
- Size: 33.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ea2d63af5962502f4a47b2ec8d232fbf9e786d77f205683c79f74606da335301
|
|
| MD5 |
e258a34a4eade3b5d71de987bee96a4c
|
|
| BLAKE2b-256 |
773539cd29708d95f3376fc7f153aa4d4fa5cdc593df0f09ba6135ed64e2234a
|
Provenance
The following attestation bundles were made for cta2045_proxy-0.2.0.tar.gz:
Publisher:
publish.yml on electrification-bus/cta2045-proxy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cta2045_proxy-0.2.0.tar.gz -
Subject digest:
ea2d63af5962502f4a47b2ec8d232fbf9e786d77f205683c79f74606da335301 - Sigstore transparency entry: 2147967025
- Sigstore integration time:
-
Permalink:
electrification-bus/cta2045-proxy@bde3e37525e7d5952687183cb0423206b736b8ec -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/electrification-bus
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bde3e37525e7d5952687183cb0423206b736b8ec -
Trigger Event:
push
-
Statement type:
File details
Details for the file cta2045_proxy-0.2.0-py3-none-any.whl.
File metadata
- Download URL: cta2045_proxy-0.2.0-py3-none-any.whl
- Upload date:
- Size: 24.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ecb933220869c4f360d77256a5190486147e45a1f9c13bc9d7f6a279d2a2a383
|
|
| MD5 |
f6fcadc6cd88ed81bfc1d8b8aaad3860
|
|
| BLAKE2b-256 |
21f8ad67442c1f3e0543c13df600a21d7418e1cc1f63da8eb2c3a87e7dd2b395
|
Provenance
The following attestation bundles were made for cta2045_proxy-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on electrification-bus/cta2045-proxy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cta2045_proxy-0.2.0-py3-none-any.whl -
Subject digest:
ecb933220869c4f360d77256a5190486147e45a1f9c13bc9d7f6a279d2a2a383 - Sigstore transparency entry: 2147967034
- Sigstore integration time:
-
Permalink:
electrification-bus/cta2045-proxy@bde3e37525e7d5952687183cb0423206b736b8ec -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/electrification-bus
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bde3e37525e7d5952687183cb0423206b736b8ec -
Trigger Event:
push
-
Statement type: