You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
homie: inbound /set async dispatch, thread-safe and promoted to the root (#15) (#23)
- Dispatch: schedule an async /set callback onto the consumer's loop with
asyncio.run_coroutine_threadsafe (ensure_future is not safe from paho's
network thread, where /set arrives). Branch on the callback's actual return
(iscoroutine), not async_loop's presence, so a SYNC callback runs inline even
when a device-level loop is set. Surface exceptions the discarded scheduling
Future would otherwise swallow, matching the sync path's logging.
- Promotion: async_loop is now Device(async_loop=), propagated to every property
via add_node()/Node.add_property() (mirroring _qos), children inheriting the
root's loop. The per-Property async_loop still works and is kept when the
device sets none.
- Fix the Optional[...] = False default to = None (bool vs loop annotation);
widen to AbstractEventLoop.
- Fix Node's mutable default properties: dict = {} (shared-dict footgun the new
propagation reads/writes through).
docs: CHANGELOG [Unreleased] (Added Device(async_loop=), Fixed dispatch/defaults);
README BYO note on async /set. Reviewed adversarially; 4 findings fixed. +10 tests.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+8Lines changed: 8 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,6 +4,14 @@ All notable changes to `ebus-sdk` are recorded here. Format follows [Keep a Chan
4
4
5
5
## [Unreleased]
6
6
7
+
### Added
8
+
9
+
-`Device(async_loop=...)`: set the consumer's asyncio event loop once on the root instead of on every settable `Property`. It propagates to every property in the tree via `add_node()` / `Node.add_property()` (like QoS) and is inherited by child devices, so inbound `/set` callbacks for a tree with N settable properties no longer configure the loop N times. The per-`Property``async_loop` argument still works and is preserved when no device-level loop is set. ([#15](https://github.com/electrification-bus/python-sdk/issues/15))
10
+
11
+
### Fixed
12
+
13
+
- Inbound `/set` on a settable property with an async (coroutine) callback is now dispatched to the consumer's event loop with `asyncio.run_coroutine_threadsafe` instead of `asyncio.ensure_future`. `/set` arrives on the transport's network-loop thread, and `ensure_future` is not safe to call from a thread other than the loop's own, so the previous path could misbehave on a real async host. The dispatch now branches on whether the callback returns a coroutine (not merely on a loop being configured), so a synchronous callback keeps running inline even when a tree-wide loop is set, and an exception raised inside an async callback is logged rather than silently swallowed by the discarded scheduling future. The `async_loop` default is corrected from `False` to `None` (a bool against the event-loop annotation, which `py.typed` surfaces), and `Node`'s mutable default `properties={}` is replaced with `None` (a shared-dict footgun the new propagation reads and writes through). ([#15](https://github.com/electrification-bus/python-sdk/issues/15))
Copy file name to clipboardExpand all lines: README.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -74,6 +74,8 @@ client.connect() # host connects on its own loop
74
74
75
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.
76
76
77
+
For the inbound direction, if the tree has settable properties whose callbacks are async coroutines, pass `Device(async_loop=<your event loop>)`: inbound `/set` arrives on the transport's network thread, and this schedules the callback onto your loop (set once for the whole tree, not per property). A synchronous callback runs inline and needs no loop.
78
+
77
79
**Two host shapes, and the will's limit.** The example above is a caller that owns a *dedicated* client and connects it itself, so it can set the will before connect. A host with a *shared* connection it does not own (Home Assistant is the archetype: one connection, up before your code loads, its will already set from the host's own config with no hook to change it) cannot set an eBus will, because MQTT allows one will per connection. There, wire `refresh_tree()` to the host's reconnect callback and let the SDK gate on `is_connected()`: the tree stays correct across every reconnect the process survives. (A consequence to be aware of: on such a shared connection the SDK cannot set the will's `$state=lost`, since the will is a CONNECT-time property the host owns.)
0 commit comments