Skip to content

homie: Device bring-your-own-transport (inject mqttc on a root Device) - #18

Merged
dcj merged 1 commit into
mainfrom
feat/device-byo-transport
Aug 2, 2026
Merged

dcj merged 1 commit into
mainfrom
feat/device-byo-transport

Conversation

@dcj

@dcj dcj commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Producer-side bring-your-own-transport: a root Device accepts a caller-supplied MQTT client so the SDK can be embedded in a host that already owns its connection. The driving case is Home Assistant, whose MQTT integration is single_config_entry (a second SDK-owned connection is impossible) and forbids background threads. This mirrors the Controller(mqttc=...) seam shipped in 0.13.0.

From discussion #7 (Bill Flood / @cayossarian). Consumes the wiring contract from #13. The MqttDeviceTransport protocol widening is a follow-up on #12 (Device's mqttc is typed Optional[MqttClient] for now, exactly as Controller shipped pre-#12).

What it does

  • Device(mqttc=<client>): root-only, mutually exclusive with mqtt_cfg= and parent=. self._owns_client = mqttc is None gates lifecycle.
  • start_mqtt_client() (both Device and Property) no-ops for an injected client: the SDK never starts a client it was handed.
  • stop() branches on ownership: owned = flush + close (unchanged); injected = plain retained $state=disconnected then return, no flush, no close (non-blocking on the caller's loop).
  • publish_value()/clear_value() gate on connectivity (is_running or is_connected()), so a loop-owning host that never calls the SDK's start() still publishes property values. Owned behavior is unchanged (there, connected implies running).
  • on_disconnect= is documented and warned as inert for an injected client (the caller registers disconnect handling on its own client, like Controller).
  • Presence-by-identity for the mqtt_cfg exclusivity guard (mqtt_cfg={} + parent now raises rather than silently dropping).

The caller wires the two Homie-correctness pieces from #13: set Device.will() on the client before connecting, and call Device.refresh_tree() from its on-connect. See the new README "Bring-your-own-transport" section.

Review

Ran a 3-lens adversarial review (correctness / edge-cases / Controller-consistency) with a per-finding refutation pass; 5 of 13 findings confirmed and all fixed here. The load-bearing one: publish_value/clear_value gated on is_running (set only by the SDK's start()), which a loop-owning HA host never calls, so every property value would have silently dropped. Now gated on connectivity, with a regression test.

Tests / docs

+11 tests (517 total, was 506 on main), including the connected-but-not-running publish regression and the ownership/exclusivity/inert-hook cases. ruff check + ruff format --check clean; the 3.10-3.13 matrix (#16) runs on this PR.

README gains a Bring-your-own-transport subsection; CHANGELOG [Unreleased] gets the Device BYO / will() / resync() entries (backfilling #13).

🤖 Generated with Claude Code

#14)

Lets a root Device accept a caller-supplied MQTT client so the SDK can be
embedded in a host that owns its connection (Home Assistant: single_config_entry
makes a second SDK-owned connection impossible). Mirrors Controller's seam.

- Device(mqttc=): root-only, mutually exclusive with mqtt_cfg= and parent=.
  _owns_client = mqttc is None gates lifecycle. Typed Optional[MqttClient] for
  now (the MqttDeviceTransport protocol widening rides #12).
- start_mqtt_client() (Device and Property) no-ops for an injected client.
- stop() branches on ownership: injected publishes a plain retained
  $state=disconnected and returns without flushing or closing (non-blocking).
- publish_value()/clear_value() gate on connectivity (is_running OR is_connected)
  so a loop-owning host that never calls start() still publishes values; owned
  behavior is unchanged (connected implies running there).
- on_disconnect= is documented and warned as inert for an injected client.
- Presence-by-identity for the mqtt_cfg exclusivity guard.

Caller wires the Homie-correctness pieces from #13: set will() before connecting
and call refresh_tree() from their on-connect.

docs: README Bring-your-own-transport section; CHANGELOG [Unreleased] entries
(also backfills #13's will()/resync()).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@dcj
dcj merged commit c7aab69 into main Aug 2, 2026
5 checks passed
@dcj
dcj deleted the feat/device-byo-transport branch August 2, 2026 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant