python-cta2045
A CTA-2045 (ANSI/CTA-2045-B) protocol library in Python — encode/decode of the demand-response interface between a Smart Grid Device (SGD, e.g. a water heater) and a Universal Communications Module (UCM).
Status: pre-alpha. The core protocol — Basic DR and Intermediate DR encode/decode, plus an abstract UCM interface — is implemented and tested. The public API may still change before 1.0, and the package is not yet published to PyPI.
Not certified. This is an independent implementation of the published protocol. It has not been tested or certified under any conformance program (EcoPort or otherwise), and carries no warranty of interoperability with any certified device.
What is CTA-2045?
CTA-2045 (consumer-facing brand: EcoPort) standardizes a modular communications socket on an appliance (the SGD) into which a UCM plugs to provide grid demand-response. CTA-2045 specifies only the SGD↔UCM link; a UCM's upward (network) interface is vendor-specific. This library implements the CTA-2045 message layer itself, independent of any vendor or transport: bytes (or ASCII-hex) in, structured Python objects out, and back. It has no runtime dependencies and does no I/O.
See References for the standard and the EcoPort program.
Install
Not yet on PyPI. For now, install from source:
pip install -e ".[dev]"
Requires Python 3.10+.
Quick start
Encode a command to send to a device, and decode messages received from one:
from cta2045 import app
from cta2045.codec import bytes_to_hex
# Encode a 10-minute shed command (UCM -> SGD). Durations are in MINUTES.
msg = app.shed(10)
msg.to_bytes() # b'\x08\x01\x00\x02\x01\x11'
bytes_to_hex(msg.to_bytes()) # '080100020111'
# Decode messages received from a device (one or more concatenated frames):
for m in app.decode_hex('080100021302'):
print(m.category, m.operational_state)
# BasicDRCategory.State_Query_Response OperationalState.Running_Curtailed
Basic DR commands
app.shed(10) # curtail load for 10 minutes
app.end() # end the current shed event
app.load_up(30) # store energy for 30 minutes
app.critical_peak(60) # critical peak event
app.grid_emergency(15) # grid emergency event
app.power_level(50) # request 50% power
Each returns a BasicDR object; call .to_bytes() for the wire frame. Durations are minutes; pass an explicit cta2045.codec.Duration for the Unknown / Too Long sentinels or for second-level control. Note the wire format quantizes durations (it can only carry 2·n² seconds) — use Duration.nearest() to see the value that will actually be transmitted.
Advanced Load Up
from cta2045 import app
from cta2045.codec import bytes_to_hex
from cta2045.enums import AdvancedLoadUpUnits
# Store 500 Wh (5 × 100 Wh) of extra energy over 60 minutes (CTA-2045-B § 11.6)
cmd = app.advanced_load_up(60, 5, AdvancedLoadUpUnits.Wh_100)
bytes_to_hex(cmd.to_bytes()) # '080200070C00003C000502'
Decoding device replies
decode_all(bytes) / decode_hex(str) return a list of message objects — BasicDR, IntermediateDR (whose .body is a CommodityReadReply, GetInformationReply, AdvancedLoadUp, or ThermostatResponse), or UnknownMessage for message types this library doesn't yet decode. Decoding is lenient: an unrecognized opcode or enum value is preserved (as category=None with a raw opcode1, or as a raw int) rather than raising — structural errors (truncated frames) still raise cta2045.codec.CodecError.
from cta2045 import app
for m in app.decode_all(raw_bytes):
if isinstance(m, app.IntermediateDR) and isinstance(m.body, app.CommodityReadReply):
for r in m.body.reports:
print(r.code, r.instantaneous, r.cumulative)
Implementing a UCM binding
cta2045.ucm.Ucm is an abstract interface that turns the codec into a UCM client. Subclass it, implement the single transport primitive transmit(), and you get the full DR command set plus inbound decoding for free:
from cta2045.ucm import Ucm
from cta2045.enums import AdvancedLoadUpUnits
class MyUcm(Ucm):
def transmit(self, frame: bytes) -> None:
... # send `frame` to the SGD over your serial link
def on_message(self, message) -> None:
... # handle one decoded inbound message
ucm = MyUcm()
ucm.shed(10) # build + transmit a shed command
ucm.advanced_load_up(60, 5, AdvancedLoadUpUnits.Wh_100)
ucm.receive(inbound_bytes) # decode + dispatch to on_message()
Vendor-proprietary UCM bindings (which add a specific UCM's network API) live in separate, non-open packages.
Package layout
cta2045.enums— on-the-wire enumerations (message/DR-command types, device types, operational states, commodity codes, capabilities, …).cta2045.app— application-layer messages: Basic DR, Intermediate DR (commodity/energy reads, GetInformation, Advanced Load Up), with encoders and decoders.cta2045.codec— ASCII-hex ↔ bytes, frame header parse/build, and field encodings (e.g. the event-duration byte).cta2045.link— reserved for a future RS-485 link-layer implementation (enabling a Pi/RS-485 "own-UCM").cta2045.ucm— abstract UCM interface; vendor-proprietary bindings live in separate packages.
Development
pip install -e ".[dev]"
pytest
ruff check . && ruff format --check .
These three checks are the quality gate; all must pass before a change is merged. See CONTRIBUTING.md for how to file issues, propose changes, and the project's spec-conformance posture.
Scope & boundaries
This is a pure codec for the CTA-2045 message/application layer — no transport, no I/O. The CTA-2045 link layer (RS-485 or SPI serial framing, ACK/NAK, CRC) is a separate concern; the cta2045.link namespace is reserved for it.
References
- ANSI/CTA-2045-B — Modular Communications Interface for Energy Management, the standard this library implements: overview · purchase / download. The Intermediate DR application messages come from the companion ANSI/CTA-2045.3.
- EcoPort — the consumer-facing brand for CTA-2045-certified products; certification is run by the OpenADR Alliance.
- Certification & testing — OpenADR and CTA-2045 / EcoPort testing (UL Solutions).
License
MIT — see LICENSE.
Release files for cta2045 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 | |
|---|---|---|---|
| cta2045-0.1.0.tar.gz | 26.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cta2045-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 47.0 kB
Release files / cta2045-0.1.0.tar.gz
| Download URL | cta2045-0.1.0.tar.gz |
|---|---|
| Size | 26.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b44f67d9080175eeeda41f7d4c08a59bdd1020a28b1e4665c325340104a15723
|
|
BLAKE2b-256 checksum How to use checksums |
a374a94d30a1dd91d5cdb23d81f738b30dcf13bcd4e37ac8549634f050e19e85
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
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 Jun 21, 2026.
Transparency logRelease files / cta2045-0.1.0-py3-none-any.whl
| Download URL | cta2045-0.1.0-py3-none-any.whl |
|---|---|
| Size | 20.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
574ebf9564d6ff57ae54ed4da2fb66bc1a501c0097add7cc3ef78266fc49ded8
|
|
BLAKE2b-256 checksum How to use checksums |
39db2c621f9e238b542159b354cdfb22db1d3e58c3f8bb5f1071fdb414220f3d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
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 Jun 21, 2026.
Transparency log