Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to `ebus-sdk` are recorded here. Format follows [Keep a Chan

## [Unreleased]

### Documentation

- `DeviceTreeBuilder` now states two parts of its contract that the API alone did not convey, both reported by a consumer reconciling an existing multi-device builder against it. First, whether a producer is expected to *adapt its own model* to `GroupedPropertyDict` or to *own one*: it is the second, which is the observable-model pattern the proxy guide prescribes, and the builder accepts rather than creates one so a single model can span a tree and so a producer holding one already can hand it over. Second, the limit of `add()`'s ordering: it orders late-bound ids and builds an unbuilt parent it was handed, but `DeviceSpec` is frozen and `parent` is a direct reference, so a child spec cannot be constructed before its parent spec exists. A caller deriving specs from a source that names parents indirectly still owns that dependency ordering. `add()` reads as though ordering is handled generally; it is handled for ids. Thanks to [@cayossarian](https://github.com/cayossarian) ([#49](https://github.com/electrification-bus/python-sdk/issues/49)).

### Added

- `node_id` on `build_from_declarations` and `DeviceTreeBuilder`: a callable mapping a capability to the Homie node id it materializes onto, defaulting to the capability itself. The node id was hardcoded to the capability name, which 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. 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. Pairs with `PropertySpec.model_group` from 0.21.0, which separates the same two instances in the model the way this separates them on the wire; using one without the other moves the collision rather than removing it. ([#47](https://github.com/electrification-bus/python-sdk/issues/47))
Expand Down
2 changes: 2 additions & 0 deletions doc/building-a-proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ Device class, id and parent are device-level facts, so they live on the `DeviceS

Four things the tree builder does that the single-device one has no need to:

- **The model is yours.** The builder accepts a `GroupedPropertyDict` and never creates one, because it is the model a producer OWNS rather than an adapter seam for a foreign model type: acquisition code writes into it and publishing follows from the bindings. Accepting rather than creating is what lets one model span a whole tree, and what lets a producer that already holds one hand it over.
- **Ordering is yours too, one step earlier.** `add()` orders late-bound ids and builds an unbuilt parent it was handed, but `DeviceSpec` is frozen and `parent` is a direct reference, so a child spec cannot be constructed before its parent spec exists. If you derive specs from a declarative source that names parents indirectly, that dependency ordering stays with you.
- **The model group is the device**, not the capability. Two children that both expose `info` collide the moment they share a capability-keyed model. A `PropertySpec` that names its own `model_group` still wins, so an existing model keyed your way keeps that keying.
- **Ids can be late-bound.** `device_id` may be a callable returning `None` while an asynchronous identifier has not arrived. `add()` returns `None` and remembers the spec; `resolve_deferred()` retries and resolves a whole generation, including children waiting behind a deferred parent. Waiting beats guessing: a child published under a wrong-but-stable id leaves retained topics that survive restarts and firmware updates.
- **`add()` is idempotent.** Incremental lifecycles re-fire, and a second `add()` of a built spec returns the same `Device` without republishing anything.
Expand Down
22 changes: 19 additions & 3 deletions src/ebus_sdk/declaration.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,12 +404,28 @@ class DeviceTreeBuilder:
This builder covers that shape, and differs from the single-device one in
four ways that all follow from there being more than one device:

1. **The model is external.** It is passed in, never created, and each
device gets its own group (its id by default). Keying by capability
would collide the moment two children both expose `info`.
1. **The model is external, and it is yours.** It is passed in, never
created, and each device gets its own group (its id by default). Keying
by capability would collide the moment two children both expose `info`.
The division: a `GroupedPropertyDict` is the model a producer is meant to
OWN, not an adapter seam a producer maps a foreign model type onto. That
is the observable-model pattern `doc/building-a-proxy.md` prescribes,
where acquisition code writes values into the model and publishing is a
reactive side effect of the bindings. The builder accepts rather than
creates one so a single model can span a whole tree, and so a producer
that already holds one (populated before any Homie tree exists) can hand
it over.
2. **Ids can be late-bound.** `add()` returns `None` for a spec whose id is
not yet knowable and remembers it; `resolve_deferred()` retries, and a
deferred parent unblocking its deferred children resolves in one call.
Note the limit of that: `add()` orders late-bound IDS, and builds an
unbuilt parent it was handed. It does not order the construction of the
specs themselves. `DeviceSpec` is frozen and `parent` is a direct
reference, so a child spec cannot exist before its parent spec does, and
a caller deriving specs from a declarative source that names parents
indirectly (by class, by type, by key) still owns that dependency
ordering. `add()` reads as though ordering is handled generally; it is
handled for ids.
3. **It is incremental.** Devices come and go over a tree's life, so `add()`
is idempotent (lifecycles re-fire) and `remove()` tears one down.
4. **Removal is depth-first**, grandchild before parent, derived from the
Expand Down