Skip to main content

OpenADR 3 companion client with VEN registration, MQTT notifications, and lifecycle management

Project description

python-oa3-client

PyPI version Python versions Lint Ruff License: MIT

OpenADR 3 companion client with VEN/BL client framework, lifecycle management, and optional MQTT and webhook notification channels.

Built on top of openadr3 (Pydantic models, httpx HTTP client).

Install

pip install python-oa3-client            # core: VEN/BL clients, API access
pip install python-oa3-client[mqtt]      # + MQTT notifications
pip install python-oa3-client[webhooks]  # + webhook receiver
pip install python-oa3-client[mdns]      # + mDNS/DNS-SD VTN discovery
pip install python-oa3-client[all]       # everything

The core package depends only on openadr3. Notification channels are optional extras:

Extra Adds Dependency
mqtt MQTT broker connection, topic discovery, message collection ebus-mqtt-client (paho-mqtt v2)
webhooks HTTP webhook receiver for VTN callbacks Flask
mdns mDNS/DNS-SD VTN discovery (_openadr3._tcp.) zeroconf
all All of the above

Architecture

BaseClient          — auth, lifecycle, __getattr__ delegation to OpenADRClient
├── VenClient       — VEN registration, program lookup, notification subscribe
└── BlClient        — thin wrapper, client_type="bl", no VEN concepts

All OpenADRClient methods (raw HTTP, coerced entities, introspection) are available directly on both client types via __getattr__ delegation — no explicit delegation methods needed.

Authentication

Two auth modes:

Direct token — provide a Bearer token directly:

ven = VenClient(url=vtn_url, token=my_token)

OAuth2 client credentials — token fetched automatically on start():

ven = VenClient(
    url=vtn_url,
    client_id="my_client",
    client_secret="my_secret",
)

For the OpenADR 3 VTN Reference Implementation, the default auth uses basic credentials encoded as base64(client_id:secret):

import base64
bl_token = base64.b64encode(b"bl_client:1001").decode()
ven_token = base64.b64encode(b"ven_client:999").decode()

mDNS/DNS-SD Discovery

Requires: pip install python-oa3-client[mdns]

The OpenADR 3.1.0 spec defines mDNS service discovery for local VTNs using service type _openadr3._tcp.. Clients can discover VTNs on the local network without a configured URL.

Discovery modes

Mode Behavior
"never" (default) Skip mDNS, use configured url. Current behavior.
"prefer_local" Try mDNS; use discovered VTN if found; fall back to url; raise if neither.
"local_with_fallback" Try mDNS; fall back to configured url if not found (requires url).
"require_local" Try mDNS; raise if no VTN found. No url needed.

Zero-config VEN

from openadr3_client import VenClient

# No URL needed — discovers VTN on the local network
with VenClient(token=token, discovery="require_local") as ven:
    ven.register("my-thermostat")
    events = ven.events()

Discovery with cloud fallback

with VenClient(
    url="https://cloud-vtn.example.com/openadr3/3.1.0",
    token=token,
    discovery="local_with_fallback",
    discovery_timeout=3.0,
) as ven:
    # Uses local VTN if found, otherwise cloud URL
    ven.register("my-thermostat")

Standalone discovery

from openadr3_client import discover_vtns

vtns = discover_vtns(timeout=3.0)
for v in vtns:
    print(f"{v.name} at {v.url} (version={v.version})")

Advertising a VTN for testing

For testing mDNS discovery without modifying the VTN itself:

from openadr3_client import advertise_vtn

with advertise_vtn(
    host="127.0.0.1",
    port=8080,
    base_path="/openadr3/3.1.0",
    local_url="http://127.0.0.1:8080/openadr3/3.1.0",
    version="3.1.0",
) as adv:
    # VTN is now visible via mDNS
    # ... run discovery tests ...
# Service unregistered on exit

VEN Client

VenClient is the primary interface for VEN developers:

from openadr3_client import VenClient

with VenClient(url="http://vtn:8080/openadr3/3.1.0", token=token) as ven:
    # Register VEN (idempotent — finds existing or creates new)
    ven.register("my-thermostat-ven")

    # Find a specific program
    pricing = ven.find_program_by_name("residential-pricing")

    # Check notification support
    if ven.vtn_supports_mqtt():
        mqtt = ven.add_mqtt("mqtts://broker:8883")
        mqtt.start()
        ven.subscribe(
            program_names=["residential-pricing"],
            objects=["EVENT"],
            operations=["CREATE", "UPDATE"],
            channel=mqtt,
        )
        msgs = mqtt.await_messages(1, timeout=30.0)
    else:
        events = ven.poll_events(program_name="residential-pricing")

    # All OpenADRClient methods work via __getattr__
    resp = ven.get_subscriptions()
    reports = ven.reports()

VEN registration

ven.register("my-ven")
print(ven.ven_id)    # "ven-abc-123"
print(ven.ven_name)  # "my-ven"

Program lookup

# Query by name (caches ID)
program = ven.find_program_by_name("residential-pricing")

# Cached name→ID resolution
pid = ven.resolve_program_id("residential-pricing")

Notifier discovery

notifiers = ven.discover_notifiers()
supports_mqtt = ven.vtn_supports_mqtt()

VEN-scoped topic methods

Default to the registered ven_id when called without arguments:

ven.register("my-ven")
resp = ven.get_mqtt_topics_ven()           # uses registered ven_id
resp = ven.get_mqtt_topics_ven_events()
resp = ven.get_mqtt_topics_ven("other-id") # explicit ven_id

BL Client

For business logic (creating programs, events):

from openadr3_client import BlClient

with BlClient(url=vtn_url, token=bl_token) as bl:
    bl.create_program({
        "programName": "tariff-program",
        "programType": "PRICING_TARIFF",
        "country": "US",
        "principalSubdivision": "CA",
        "intervalPeriod": {"start": "2024-01-01T00:00:00Z", "duration": "P1Y"},
    })
    bl.create_event({...})

Notification Channels

MqttChannel

Requires: pip install python-oa3-client[mqtt]

mqtt = ven.add_mqtt("mqtt://broker:1883", client_id="my-ven-mqtt")
mqtt.start()

# Manual topic subscription
mqtt.subscribe_topics(["openadr3/#"])

# Or use ven.subscribe() for program-aware subscription
ven.subscribe(
    program_names=["residential-pricing"],
    objects=["EVENT"],
    operations=["CREATE", "UPDATE"],
    channel=mqtt,
)

msgs = mqtt.await_messages(n=1, timeout=10.0)
for msg in msgs:
    print(msg.topic, msg.payload)

mqtt.stop()

TLS connections: use mqtts:// scheme (default port 8883).

WebhookChannel

Requires: pip install python-oa3-client[webhooks]

webhook = ven.add_webhook(
    port=0,                        # OS-assigned ephemeral port
    bearer_token="my-secret",
    callback_host="192.168.1.50",  # IP reachable from VTN
)
webhook.start()
print(webhook.callback_url)  # "http://192.168.1.50:54321/notifications"

# Subscribe creates VTN subscription with callback URL
ven.subscribe(
    program_names=["residential-pricing"],
    objects=["EVENT"],
    operations=["CREATE", "UPDATE"],
    channel=webhook,
)

msgs = webhook.await_messages(n=1, timeout=10.0)
webhook.stop()

Channel lifecycle

Channels are created but not started automatically. You control the lifecycle:

mqtt = ven.add_mqtt(broker_url)  # Created, not connected
mqtt.start()                      # Connected
# ... use ...
mqtt.stop()                       # Disconnected

When VenClient stops (via stop() or context manager exit), all channels are stopped automatically.

Message types

MQTTMessage:

Field Type Description
topic str MQTT topic
payload Any Parsed JSON, or coerced Notification
time float Unix timestamp
raw_payload bytes Original bytes

WebhookMessage:

Field Type Description
path str URL path
payload Any Parsed JSON, or coerced Notification
time float Unix timestamp
raw_payload bytes Original request body

Direct API access

All OpenADRClient methods are available on both VenClient and BlClient via __getattr__:

# Raw HTTP (returns httpx.Response)
resp = ven.get_programs(skip=0, limit=10)
resp = ven.create_subscription({...})

# Coerced entities (returns Pydantic models)
programs = ven.programs()
event = ven.event("evt-001")
reports = ven.reports()
subscriptions = ven.subscriptions()

# Introspection (requires spec_path)
routes = ven.all_routes()
scopes = ven.endpoint_scopes("/programs", "get")

Low-level components

The standalone MQTTConnection, WebhookReceiver, extract_topics, normalize_broker_uri, and detect_lan_ip are still exported for direct use.

Examples

Development

git clone https://github.com/grid-coordination/python-oa3-client
cd python-oa3-client
pip install -e ".[dev]"
pytest tests/ -v

License

MIT License — Copyright (c) 2026 Clark Communications Corporation

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

python_oa3_client-0.2.1.tar.gz (25.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

python_oa3_client-0.2.1-py3-none-any.whl (20.0 kB view details)

Uploaded Python 3

File details

Details for the file python_oa3_client-0.2.1.tar.gz.

File metadata

  • Download URL: python_oa3_client-0.2.1.tar.gz
  • Upload date:
  • Size: 25.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for python_oa3_client-0.2.1.tar.gz
Algorithm Hash digest
SHA256 9ebdc92f5ff6ffc0d806339982d8bf26ab6ce0b31a44b898cfa8c3308f432819
MD5 d191127662e1de26d959c2268090b958
BLAKE2b-256 96fd1f2f783cad606b6d1e9520307ba76d0ed379ac8c09f8ac69b2ce2407ed9d

See more details on using hashes here.

Provenance

The following attestation bundles were made for python_oa3_client-0.2.1.tar.gz:

Publisher: publish.yml on grid-coordination/python-oa3-client

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file python_oa3_client-0.2.1-py3-none-any.whl.

File metadata

File hashes

Hashes for python_oa3_client-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 aa0a86801d57bffb3a5f284d2e9898c91d8876061a7e14289bd976bb0b94e9c1
MD5 48e76872702b61e5a63024a2bda3ec46
BLAKE2b-256 a2baad7203a9f3ab195d67803429a5483e46dbb299e8f537cdf05d0c7de958b6

See more details on using hashes here.

Provenance

The following attestation bundles were made for python_oa3_client-0.2.1-py3-none-any.whl:

Publisher: publish.yml on grid-coordination/python-oa3-client

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page