feat: node_id callable so one device can carry two instances of a capability - #64
Merged
Conversation
…ability
The Homie node id was hardcoded to the capability name:
device.add_node_from_dict(
{"id": capability, "name": node_name(capability), ...}
)
That is right until one device carries two instances of the same
capability (two lugs, two meters), at which point the second silently
lands on the first one's node. node_type and node_name were already
callables, so the id was the one part of a node a caller could not
choose.
Adds node_id to build_from_declarations and DeviceTreeBuilder, threaded
through _materialize, defaulting to identity.
Renaming is all it does. The declaration's vocabulary stays `capability`,
the model group still comes from the spec, and the returned map is still
keyed by the declared capability, so a caller who ignores it sees no
change; the 653 tests that passed before still pass untouched.
It pairs with PropertySpec.model_group from 0.21.0: node_id separates two
instances on the wire, model_group separates them in the model. Using one
without the other moves the collision rather than removing it, which the
docstrings and the guide now say outright.
This is the SDK half of reconciling panel-sim's wire/graph_builder with
DeviceTreeBuilder. With it, `node-on-parent` placement needs no placement
enum in the SDK: it stops being a tree concern and becomes "add more
nodes to a device that already exists", which build_from_declarations
already does. Design pass recorded separately.
Refs #47
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dcj
added a commit
that referenced
this pull request
Aug 21, 2026
…n) (#71) Bumps __version__ to 0.22.0 (single source of truth), promotes Unreleased to [0.22.0], and adds the tag link definition. #64 node_id callable, so one device can carry two instances of a capability #65 DeviceTreeBuilder contract: model ownership, and the limit of add()'s ordering #66 the builder replaced properties in a model it does not own, discarding live values, callbacks and entity_setters. A producer that wired inbound control lost the actuator while $description kept advertising settable: true. Data loss in 0.21.0's headline feature. #67 a child could silently shadow an ancestor's id, or a sibling's, putting two devices on the same topics. Plus the feature it was filed for: add_root_capabilities(), so a root can carry its own capabilities rather than only parenting children. #68 extend(), so a device that already exists can grow a capability. The previous workaround dropped properties from $description while leaving their retained topics on the broker. Minor rather than patch: three new public methods. #66's fix is the reason not to sit on it, since 0.21.0 is live with that data loss. Materialization is now idempotent at three levels (node, property, and the state transition itself), which is what makes a re-fired incremental lifecycle a genuine no-op. The third matters most: an empty state_transition still emits init -> ready, and that edge forces every controller on the bus to resync. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Refs #47. The SDK half of reconciling panel-sim's
wire/graph_builderwith theDeviceTreeBuilderthat shipped in 0.21.0.The gap
_materializehardcoded the Homie node id to the capability name:node_typeandnode_namewere already callables, so the id was the one part of a node a caller could not choose. That is fine until one device carries two instances of the same capability (two lugs, two meters), at which point the second silently lands on the first one's node.The change
node_id: Callable[[str], str]onbuild_from_declarationsandDeviceTreeBuilder, threaded through_materialize, defaulting to identity.Renaming is all it does:
capabilitySo a caller who ignores it sees no change, which the 653 pre-existing tests confirm by passing untouched.
It pairs with
model_groupnode_idseparates two instances on the wire;PropertySpec.model_group(0.21.0) separates them in the model. Using one without the other moves the collision rather than removing it, so the docstrings anddoc/building-a-proxy.mdsay that outright rather than leaving it to be discovered.test_two_instances_of_one_capability_coexist_on_one_deviceis the shape reduced to essentials: two lugs on one enclosure, onebuild_from_declarationscall per instance, distinctnode_idand distinctmodel_group, then both published and read back independently.Why this is the whole SDK-side change
The design pass over
graph_builderfound its two unique capabilities (node-on-parentplacement, and per-instance node-id templating) reduce to this one missing seam. With it:node-on-parentneeds no placement enum in the SDK. It stops being a tree concern and becomes "add more nodes to a device that already exists", whichbuild_from_declarationsalready does. The front end calls it against the parent withnode_id=lambda cap: f"{prefix}-{cap}".instance_idis.Everything else
graph_builderdoes is either already covered (topological sort falls out of recursiveadd(); parent-by-class resolution happens while building specs) or better left where it is (root construction frommqtt_cfg=/mqttc=is connection policy, and belongs to the caller).Verification
ruff checkandruff format --checkcleanCHANGELOG.mdanddoc/building-a-proxy.mdnode_typeandnode_nameare not documented there either, and that section defers parameter-level detail to the proxy guide🤖 Generated with Claude Code