Skip to content

Commit 195d3d8

Browse files
committed
Merge branch 'main' into pr12-finish
2 parents 266e329 + c7aab69 commit 195d3d8

6 files changed

Lines changed: 348 additions & 23 deletions

File tree

.github/workflows/test.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
pytest:
10+
name: pytest (py${{ matrix.python-version }})
11+
runs-on: ubuntu-latest
12+
strategy:
13+
# Run every version even if one fails, so a single-version break is
14+
# visible as exactly that rather than masking the rest.
15+
fail-fast: false
16+
matrix:
17+
# Mirror pyproject's requires-python (>=3.10) and the classifiers.
18+
# CI previously ran only 3.12, so 3.10/3.11/3.13 were unverified
19+
# despite being declared supported (the StrEnum polyfill in homie.py
20+
# exists precisely for <3.11).
21+
python-version: ["3.10", "3.11", "3.12", "3.13"]
22+
steps:
23+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
24+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
25+
with:
26+
python-version: ${{ matrix.python-version }}
27+
- run: pip install -e '.[dev]'
28+
- run: python -m pytest -q

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to `ebus-sdk` are recorded here. Format follows [Keep a Chan
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- `Device` bring-your-own-transport: `Device(..., mqttc=<client>)` accepts a pre-built MQTT client instead of constructing one from `mqtt_cfg`, so a host that already owns its MQTT connection (e.g. a Home Assistant integration, whose MQTT integration is `single_config_entry` and forbids background threads) can publish an eBus device tree over its own transport and event loop. This is the producer-side mirror of the `Controller` seam added in 0.13.0. An injected client is used as-is: the SDK never `start()`s or `stop()`s it, and `stop()` publishes a final retained `$state=disconnected` through it and returns without flushing or closing (non-blocking on the caller's loop). Root-only, and mutually exclusive with `mqtt_cfg=` / `parent=`. Property publishing now gates on connectivity (`is_connected()`) rather than only the SDK-owned run flag, so a caller-driven client that never calls the SDK's `start()` still publishes property values (owned behavior is unchanged: there, connected implies running). Because an injected client bypasses the SDK's connect path (where the LWT, the reconnect republish, and the disconnect hook are wired), the caller wires the Homie-correctness pieces itself using `Device.will()` and `Device.refresh_tree()` (below); `on_disconnect=` is inert for an injected client (documented, and warned at construction). Additive: the default (no `mqttc`) path constructs, starts, owns, and stops a client exactly as before. Thanks to @cayossarian (GH #7). (#14)
10+
- `Device.will()`: returns the tree root's Last Will and Testament descriptor (topic ending in `/$state`, payload `lost`), exposed so a bring-your-own-transport caller can set it on their client before connecting: the will rides the MQTT CONNECT packet, so the SDK cannot add it to a client it is merely handed. The SDK uses it for any client it builds. `Device.refresh_tree()` (which republishes the whole tree) is documented as the companion on-connect hook a BYO caller wires so the retained tree re-announces after a reconnect. Additive. (#13)
11+
- `Controller.resync()`: the tree-rooted discovery bookkeeping reset, extracted from the owned-client on-connect handler and made public so a bring-your-own-transport tree-rooted controller can re-walk the tree from a broker reconnect it drives itself (an injected client bypasses the SDK's on-connect, where the reset is otherwise wired). A no-op in wildcard and single-device modes. Additive. (#13)
12+
713
## [0.15.0] — 2026-08-02
814

915
### Added

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,24 @@ Constructing a `Device` never blocks or fails just because the broker is momenta
5555

5656
#### Transport-free construction
5757

58-
A `Device` tree can also be built with no transport at all: pass `mqtt_cfg=None` (the default) and the tree composes `$description`, resolves ids and topics, and holds property values without opening a socket: useful for tests, for deriving the wire schema, and for a host that publishes through its own MQTT client. `mqtt_cfg={}` still connects on the transport's defaults; only `None` skips the connection. Children attach to a transport-free root exactly as they do to a connected one.
58+
A `Device` tree can also be built with no transport at all: pass `mqtt_cfg=None` (the default) and the tree composes `$description`, resolves ids and topics, and holds property values without opening a socket: useful for tests and for deriving the wire schema offline. (A host that wants to *publish* an eBus tree through its own connected client uses bring-your-own-transport, below, not this.) `mqtt_cfg={}` still connects on the transport's defaults; only `None` skips the connection. Children attach to a transport-free root exactly as they do to a connected one.
59+
60+
#### Bring-your-own-transport
61+
62+
A host that already owns its MQTT connection can publish an eBus device tree through it instead of letting the SDK open its own: pass a pre-built client as `Device(..., mqttc=client)` (root-only, mutually exclusive with `mqtt_cfg=`). The SDK uses the client as-is and never `start()`s or `stop()`s it: the caller owns its lifecycle and event loop. This is the producer-side mirror of `Controller(mqttc=...)`, and the case it exists for is a host like Home Assistant, whose MQTT integration is `single_config_entry` (a second SDK-owned connection is not an option) and forbids background threads.
63+
64+
Inject the client before it connects, then wire the two Homie-correctness pieces the SDK can only set through its own connect path: the Last Will, and the whole-tree republish on (re)connect.
65+
66+
```python
67+
client = my_host_mqtt_client() # created, not yet connected
68+
device = Device('panel-1', type='...', mqttc=client)
69+
70+
client.set_will(**device.will()) # LWT ($state=lost); must precede connect
71+
client.on_connect(device.refresh_tree) # re-announce the retained tree on every (re)connect
72+
client.connect() # host connects on its own loop
73+
```
74+
75+
`device.will()` returns the tree's Last Will descriptor and `device.refresh_tree()` republishes the whole tree; the `set_will` / `on_connect` / `connect` calls above are illustrative of your host's own MQTT API. Property values publish once the client is connected (the SDK gates on `is_connected()`, not on its own `start()`, which a caller-driven client never calls). `device.stop()` publishes a final retained `$state=disconnected` through the client and returns immediately, without flushing or closing it. `on_disconnect=` is inert for an injected client; register disconnect handling on your own client.
5976

6077
#### Clearing a value vs. an empty-string value
6178

0 commit comments

Comments
 (0)