python-oa3-client 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ id-token: write
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
17
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
18
+ with:
19
+ python-version: "3.12"
20
+ - run: pip install -e ".[dev]"
21
+ - run: pytest tests/ -v
22
+
23
+ publish:
24
+ needs: test
25
+ runs-on: ubuntu-latest
26
+ environment: pypi
27
+ steps:
28
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
29
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
30
+ with:
31
+ python-version: "3.12"
32
+ - run: pip install build
33
+ - run: python -m build
34
+ - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
35
+ with:
36
+ print-hash: true
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ *.egg
8
+ .venv/
9
+ *.so
10
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clark Communications Corporation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,387 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-oa3-client
3
+ Version: 0.1.0
4
+ Summary: OpenADR 3 companion client with VEN registration, MQTT notifications, and lifecycle management
5
+ Project-URL: Homepage, https://github.com/grid-coordination/python-oa3-client
6
+ Project-URL: Repository, https://github.com/grid-coordination/python-oa3-client
7
+ Project-URL: Issues, https://github.com/grid-coordination/python-oa3-client/issues
8
+ Author: Grid-Coordination Contributors
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: demand-response,mqtt,openadr,openadr3,ven
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: openadr3>=0.1.0
23
+ Provides-Extra: all
24
+ Requires-Dist: ebus-mqtt-client>=0.1.0; extra == 'all'
25
+ Requires-Dist: flask>=3.0.0; extra == 'all'
26
+ Requires-Dist: zeroconf>=0.131.0; extra == 'all'
27
+ Provides-Extra: dev
28
+ Requires-Dist: ebus-mqtt-client>=0.1.0; extra == 'dev'
29
+ Requires-Dist: flask>=3.0.0; extra == 'dev'
30
+ Requires-Dist: pytest; extra == 'dev'
31
+ Requires-Dist: zeroconf>=0.131.0; extra == 'dev'
32
+ Provides-Extra: mdns
33
+ Requires-Dist: zeroconf>=0.131.0; extra == 'mdns'
34
+ Provides-Extra: mqtt
35
+ Requires-Dist: ebus-mqtt-client>=0.1.0; extra == 'mqtt'
36
+ Provides-Extra: webhooks
37
+ Requires-Dist: flask>=3.0.0; extra == 'webhooks'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # python-oa3-client
41
+
42
+ OpenADR 3 companion client with VEN/BL client framework, lifecycle management, and optional MQTT and webhook notification channels.
43
+
44
+ Built on top of [openadr3](https://github.com/grid-coordination/python-oa3) (Pydantic models, httpx HTTP client).
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install python-oa3-client # core: VEN/BL clients, API access
50
+ pip install python-oa3-client[mqtt] # + MQTT notifications
51
+ pip install python-oa3-client[webhooks] # + webhook receiver
52
+ pip install python-oa3-client[mdns] # + mDNS/DNS-SD VTN discovery
53
+ pip install python-oa3-client[all] # everything
54
+ ```
55
+
56
+ The core package depends only on `openadr3`. Notification channels are optional extras:
57
+
58
+ | Extra | Adds | Dependency |
59
+ |-------|------|------------|
60
+ | `mqtt` | MQTT broker connection, topic discovery, message collection | [ebus-mqtt-client](https://github.com/electrification-bus/ebus-mqtt-client) (paho-mqtt v2) |
61
+ | `webhooks` | HTTP webhook receiver for VTN callbacks | [Flask](https://flask.palletsprojects.com/) |
62
+ | `mdns` | mDNS/DNS-SD VTN discovery (`_openadr3._tcp.`) | [zeroconf](https://github.com/python-zeroconf/python-zeroconf) |
63
+ | `all` | All of the above | — |
64
+
65
+ ## Architecture
66
+
67
+ ```
68
+ BaseClient — auth, lifecycle, __getattr__ delegation to OpenADRClient
69
+ ├── VenClient — VEN registration, program lookup, notification subscribe
70
+ └── BlClient — thin wrapper, client_type="bl", no VEN concepts
71
+ ```
72
+
73
+ All `OpenADRClient` methods (raw HTTP, coerced entities, introspection) are available directly on both client types via `__getattr__` delegation — no explicit delegation methods needed.
74
+
75
+ ## Authentication
76
+
77
+ Two auth modes:
78
+
79
+ **Direct token** — provide a Bearer token directly:
80
+ ```python
81
+ ven = VenClient(url=vtn_url, token=my_token)
82
+ ```
83
+
84
+ **OAuth2 client credentials** — token fetched automatically on `start()`:
85
+ ```python
86
+ ven = VenClient(
87
+ url=vtn_url,
88
+ client_id="my_client",
89
+ client_secret="my_secret",
90
+ )
91
+ ```
92
+
93
+ For the **OpenADR 3 VTN Reference Implementation**, the default auth uses basic credentials encoded as `base64(client_id:secret)`:
94
+
95
+ ```python
96
+ import base64
97
+ bl_token = base64.b64encode(b"bl_client:1001").decode()
98
+ ven_token = base64.b64encode(b"ven_client:999").decode()
99
+ ```
100
+
101
+ ## mDNS/DNS-SD Discovery
102
+
103
+ Requires: `pip install python-oa3-client[mdns]`
104
+
105
+ 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.
106
+
107
+ ### Discovery modes
108
+
109
+ | Mode | Behavior |
110
+ |------|----------|
111
+ | `"never"` (default) | Skip mDNS, use configured `url`. Current behavior. |
112
+ | `"prefer_local"` | Try mDNS; use discovered VTN if found; fall back to `url`; raise if neither. |
113
+ | `"local_with_fallback"` | Try mDNS; fall back to configured `url` if not found (requires `url`). |
114
+ | `"require_local"` | Try mDNS; raise if no VTN found. No `url` needed. |
115
+
116
+ ### Zero-config VEN
117
+
118
+ ```python
119
+ from openadr3_client import VenClient
120
+
121
+ # No URL needed — discovers VTN on the local network
122
+ with VenClient(token=token, discovery="require_local") as ven:
123
+ ven.register("my-thermostat")
124
+ events = ven.events()
125
+ ```
126
+
127
+ ### Discovery with cloud fallback
128
+
129
+ ```python
130
+ with VenClient(
131
+ url="https://cloud-vtn.example.com/openadr3/3.1.0",
132
+ token=token,
133
+ discovery="local_with_fallback",
134
+ discovery_timeout=3.0,
135
+ ) as ven:
136
+ # Uses local VTN if found, otherwise cloud URL
137
+ ven.register("my-thermostat")
138
+ ```
139
+
140
+ ### Standalone discovery
141
+
142
+ ```python
143
+ from openadr3_client import discover_vtns
144
+
145
+ vtns = discover_vtns(timeout=3.0)
146
+ for v in vtns:
147
+ print(f"{v.name} at {v.url} (version={v.version})")
148
+ ```
149
+
150
+ ### Advertising a VTN for testing
151
+
152
+ For testing mDNS discovery without modifying the VTN itself:
153
+
154
+ ```python
155
+ from openadr3_client import advertise_vtn
156
+
157
+ with advertise_vtn(
158
+ host="127.0.0.1",
159
+ port=8080,
160
+ base_path="/openadr3/3.1.0",
161
+ local_url="http://127.0.0.1:8080/openadr3/3.1.0",
162
+ version="3.1.0",
163
+ ) as adv:
164
+ # VTN is now visible via mDNS
165
+ # ... run discovery tests ...
166
+ # Service unregistered on exit
167
+ ```
168
+
169
+ ## VEN Client
170
+
171
+ `VenClient` is the primary interface for VEN developers:
172
+
173
+ ```python
174
+ from openadr3_client import VenClient
175
+
176
+ with VenClient(url="http://vtn:8080/openadr3/3.1.0", token=token) as ven:
177
+ # Register VEN (idempotent — finds existing or creates new)
178
+ ven.register("my-thermostat-ven")
179
+
180
+ # Find a specific program
181
+ pricing = ven.find_program_by_name("residential-pricing")
182
+
183
+ # Check notification support
184
+ if ven.vtn_supports_mqtt():
185
+ mqtt = ven.add_mqtt("mqtts://broker:8883")
186
+ mqtt.start()
187
+ ven.subscribe(
188
+ program_names=["residential-pricing"],
189
+ objects=["EVENT"],
190
+ operations=["CREATE", "UPDATE"],
191
+ channel=mqtt,
192
+ )
193
+ msgs = mqtt.await_messages(1, timeout=30.0)
194
+ else:
195
+ events = ven.poll_events(program_name="residential-pricing")
196
+
197
+ # All OpenADRClient methods work via __getattr__
198
+ resp = ven.get_subscriptions()
199
+ reports = ven.reports()
200
+ ```
201
+
202
+ ### VEN registration
203
+
204
+ ```python
205
+ ven.register("my-ven")
206
+ print(ven.ven_id) # "ven-abc-123"
207
+ print(ven.ven_name) # "my-ven"
208
+ ```
209
+
210
+ ### Program lookup
211
+
212
+ ```python
213
+ # Query by name (caches ID)
214
+ program = ven.find_program_by_name("residential-pricing")
215
+
216
+ # Cached name→ID resolution
217
+ pid = ven.resolve_program_id("residential-pricing")
218
+ ```
219
+
220
+ ### Notifier discovery
221
+
222
+ ```python
223
+ notifiers = ven.discover_notifiers()
224
+ supports_mqtt = ven.vtn_supports_mqtt()
225
+ ```
226
+
227
+ ### VEN-scoped topic methods
228
+
229
+ Default to the registered `ven_id` when called without arguments:
230
+
231
+ ```python
232
+ ven.register("my-ven")
233
+ resp = ven.get_mqtt_topics_ven() # uses registered ven_id
234
+ resp = ven.get_mqtt_topics_ven_events()
235
+ resp = ven.get_mqtt_topics_ven("other-id") # explicit ven_id
236
+ ```
237
+
238
+ ## BL Client
239
+
240
+ For business logic (creating programs, events):
241
+
242
+ ```python
243
+ from openadr3_client import BlClient
244
+
245
+ with BlClient(url=vtn_url, token=bl_token) as bl:
246
+ bl.create_program({
247
+ "programName": "tariff-program",
248
+ "programType": "PRICING_TARIFF",
249
+ "country": "US",
250
+ "principalSubdivision": "CA",
251
+ "intervalPeriod": {"start": "2024-01-01T00:00:00Z", "duration": "P1Y"},
252
+ })
253
+ bl.create_event({...})
254
+ ```
255
+
256
+ ## Notification Channels
257
+
258
+ ### MqttChannel
259
+
260
+ Requires: `pip install python-oa3-client[mqtt]`
261
+
262
+ ```python
263
+ mqtt = ven.add_mqtt("mqtt://broker:1883", client_id="my-ven-mqtt")
264
+ mqtt.start()
265
+
266
+ # Manual topic subscription
267
+ mqtt.subscribe_topics(["openadr3/#"])
268
+
269
+ # Or use ven.subscribe() for program-aware subscription
270
+ ven.subscribe(
271
+ program_names=["residential-pricing"],
272
+ objects=["EVENT"],
273
+ operations=["CREATE", "UPDATE"],
274
+ channel=mqtt,
275
+ )
276
+
277
+ msgs = mqtt.await_messages(n=1, timeout=10.0)
278
+ for msg in msgs:
279
+ print(msg.topic, msg.payload)
280
+
281
+ mqtt.stop()
282
+ ```
283
+
284
+ TLS connections: use `mqtts://` scheme (default port 8883).
285
+
286
+ ### WebhookChannel
287
+
288
+ Requires: `pip install python-oa3-client[webhooks]`
289
+
290
+ ```python
291
+ webhook = ven.add_webhook(
292
+ port=0, # OS-assigned ephemeral port
293
+ bearer_token="my-secret",
294
+ callback_host="192.168.1.50", # IP reachable from VTN
295
+ )
296
+ webhook.start()
297
+ print(webhook.callback_url) # "http://192.168.1.50:54321/notifications"
298
+
299
+ # Subscribe creates VTN subscription with callback URL
300
+ ven.subscribe(
301
+ program_names=["residential-pricing"],
302
+ objects=["EVENT"],
303
+ operations=["CREATE", "UPDATE"],
304
+ channel=webhook,
305
+ )
306
+
307
+ msgs = webhook.await_messages(n=1, timeout=10.0)
308
+ webhook.stop()
309
+ ```
310
+
311
+ ### Channel lifecycle
312
+
313
+ Channels are created but not started automatically. You control the lifecycle:
314
+
315
+ ```python
316
+ mqtt = ven.add_mqtt(broker_url) # Created, not connected
317
+ mqtt.start() # Connected
318
+ # ... use ...
319
+ mqtt.stop() # Disconnected
320
+ ```
321
+
322
+ When VenClient stops (via `stop()` or context manager exit), all channels are stopped automatically.
323
+
324
+ ### Message types
325
+
326
+ **MQTTMessage:**
327
+
328
+ | Field | Type | Description |
329
+ |-------|------|-------------|
330
+ | `topic` | `str` | MQTT topic |
331
+ | `payload` | `Any` | Parsed JSON, or coerced `Notification` |
332
+ | `time` | `float` | Unix timestamp |
333
+ | `raw_payload` | `bytes` | Original bytes |
334
+
335
+ **WebhookMessage:**
336
+
337
+ | Field | Type | Description |
338
+ |-------|------|-------------|
339
+ | `path` | `str` | URL path |
340
+ | `payload` | `Any` | Parsed JSON, or coerced `Notification` |
341
+ | `time` | `float` | Unix timestamp |
342
+ | `raw_payload` | `bytes` | Original request body |
343
+
344
+ ## Direct API access
345
+
346
+ All `OpenADRClient` methods are available on both VenClient and BlClient via `__getattr__`:
347
+
348
+ ```python
349
+ # Raw HTTP (returns httpx.Response)
350
+ resp = ven.get_programs(skip=0, limit=10)
351
+ resp = ven.create_subscription({...})
352
+
353
+ # Coerced entities (returns Pydantic models)
354
+ programs = ven.programs()
355
+ event = ven.event("evt-001")
356
+ reports = ven.reports()
357
+ subscriptions = ven.subscriptions()
358
+
359
+ # Introspection (requires spec_path)
360
+ routes = ven.all_routes()
361
+ scopes = ven.endpoint_scopes("/programs", "get")
362
+ ```
363
+
364
+ ## Low-level components
365
+
366
+ The standalone `MQTTConnection`, `WebhookReceiver`, `extract_topics`, `normalize_broker_uri`, and `detect_lan_ip` are still exported for direct use.
367
+
368
+ ## Examples
369
+
370
+ - [`examples/smoke_test.py`](examples/smoke_test.py) — integration test against live VTN-RI and Mosquitto
371
+ - [`examples/smoke_test_mdns.py`](examples/smoke_test_mdns.py) — mDNS discovery integration test (advertise + discover + connect)
372
+ - [`examples/ven_workflow.py`](examples/ven_workflow.py) — documented VEN developer workflow
373
+ - [`examples/ven_mdns.py`](examples/ven_mdns.py) — zero-config VEN that discovers its VTN via mDNS
374
+ - [`doc/ven-bl-client-guide.md`](doc/ven-bl-client-guide.md) — VEN & BL client use-case walkthrough
375
+
376
+ ## Development
377
+
378
+ ```bash
379
+ git clone https://github.com/grid-coordination/python-oa3-client
380
+ cd python-oa3-client
381
+ pip install -e ".[dev]"
382
+ pytest tests/ -v
383
+ ```
384
+
385
+ ## License
386
+
387
+ [MIT License](LICENSE) — Copyright (c) 2026 Clark Communications Corporation