Repository navigation
fix: honour the documented mqtt_cfg=None default instead of raising - #10
Merged
dcj merged 1 commit intoAug 2, 2026
Merged
Conversation
Device.__init__ called connect_broker() unconditionally for roots, so the
annotated `mqtt_cfg: Optional[dict] = None` reached MqttClient.from_config(None)
and died on None.get("host", ...). Constructing a root with its own default
argument raised AttributeError.
Skip the connect when mqtt_cfg is None, giving a device tree with no transport:
it still composes $description, resolves ids and topics, and holds property
values, but never publishes. mqtt_cfg={} keeps its existing meaning, so the only
input whose behaviour changes is the one that previously raised.
The child guard is qualified to match. It treated any clientless root as an
error, conflating "never started / already stopped" with "transport-free by
design"; only the former is a mistake. This adds no new state to the class —
stop() already leaves root.mqttc as None, and every consumer path already
tolerates it.
Closes #9
dcj
approved these changes
Aug 2, 2026
dcj
left a comment
Contributor
There was a problem hiding this comment.
Thanks, this is a clean fix for a real regression (the documented mqtt_cfg=None default raising), and the transport-free Device tree it unlocks is the producer-side twin of the 0.13.0 bring-your-own-transport seam. Verified locally: your 6 tests pass, full suite green (486), ruff clean. Merging into the 0.14.0 line; #8 and #11 tracked as follow-ups.
dcj
added a commit
that referenced
this pull request
Aug 2, 2026
…p flat controller) Bumps __version__ to 0.14.0 and cuts CHANGELOG [Unreleased] to [0.14.0]. Added: Property.set_format() (SDK-6do.2), examples/simple-tree-device (SDK-5le). Fixed: Device(mqtt_cfg=None) no longer raises and builds a transport-free tree (#9/#10, thanks @cayossarian). Removed: examples/simple-controller (flat tree-unaware discovery is the dead model; SDK-4zv tracks the tree-aware replacement). README documents transport-free construction. All additive; no signature or semantics change to the existing API. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
dcj
added a commit
that referenced
this pull request
Aug 7, 2026
* fix: publish a device's $state after its children in refresh_tree refresh_tree() walked the tree pre-order, publishing a device completely -- description, nodes, state -- before recursing. The root therefore announced $state=ready while the children its own $description names had published nothing at all. On a 37-device enclosure tree the root went ready at publish 1 and the last child at publish 73. This is the normal path, not an edge case: on_connect calls refresh_tree() for SDK-owned clients on both the initial connect and every reconnect. Homie 5 invites a controller to gate on the root's $state, and one that does proceeded against a tree whose children had not described themselves. After a broker restart a previously-healthy consumer re-read a root claiming ready with nothing under it. Description and nodes now publish first, then descendants, then this device's own state. The set of messages is unchanged and only the order differs: on the same tree, 74 publishes and 74 unique topics before and after, identical topic sets, with the root's $state moving from index 1 to index 73. The regression test fails on main and passes here. The existing suite passes either way -- no test asserted the old order, which is why it survived. * review fixups: ruff format, drop orphaned _publish_self, depth-2 order assertion - ruff format collapse on child_states (the only CI blocker; our pin is 0.15.21) - delete Device._publish_self(), which this change left with zero callers and a docstring still claiming it serves the reconnect cascade - assert the state-after-children rule at depth 2 in the existing three-level test: a fix that reordered only the root passed all 546 tests as written - reframe the comment and CHANGELOG entry as a producer-side narrowing rather than a guarantee consumers may build on, and add the LWT/atomic-commit reason (a lost root makes every child lost, so the final publish is one atomic flip) - credit @cayossarian in the CHANGELOG, matching #10/#12/#20 Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> --------- Co-authored-by: Donald Clark Jackson <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #9 — constructing a root
Devicewith its own documented default raisedAttributeError, because__init__calledconnect_broker()unconditionally for roots andmqtt_cfg=NonereachedMqttClient.from_config(None).The change
mqtt_cfg=Nonenow means no transport: the tree composes$description, resolves ids andtopics, and holds property values, but never publishes.
mqtt_cfg={}is untouched and stillconnects on
ebus-mqtt-client's defaults, so the only input whose behaviour changes is theone that previously raised.
The child guard at
:1227is qualified to match. It treated any clientless root as an error,conflating "never started / already stopped" with "transport-free by design" — only the first
is a mistake:
Both expressions carry a short comment, since the shapes are load-bearing: a truthiness test
would fold
{}into the no-transport path, and dropping the_mqtt_cfgterm would make everytransport-free tree single-node.
No new state
stop()already ends withroot.mqttc = None(:1456), and every consumer path alreadytolerates a missing client —
publish,publish_empty,subscribe,get_mqtt_client,start_mqtt_client,is_connected,stop.:1227was the only place where a clientless treewas fatal rather than quiet. This makes an existing state reachable by construction rather than
only via
stop().Tests
Six regression tests in
TestDeviceWithoutTransport:mqtt_cfg={}still connects — guards the backward-compatibility claim above$description, includingparent/rootand unit round-tripset_valueandstop()on it are no-ops rather than errorspytest: 462 passed.ruff check .andruff format --check .clean under the pinned0.15.21. (
tests/test_json_format.pyhas 5 failures on my machine that reproduce identicallyon an unmodified checkout — a missing
jsonschemalocally, unrelated to this change.)Not included
Issue #9 notes that a transport-free tree emits ~1,593 WARNING lines for a 31-device model,
because a missing client is logged at WARNING in twelve places. Correct when you meant to
publish, noise when you did not — but it is a logging-policy call across
Property,Node,and
Device, so it is deliberately not bundled here. Happy to send it separately if you wantit.