diff --git a/CHANGELOG.md b/CHANGELOG.md index 898c58a..a2d46b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/doc/building-a-proxy.md b/doc/building-a-proxy.md index b83e6cf..2897ada 100644 --- a/doc/building-a-proxy.md +++ b/doc/building-a-proxy.md @@ -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. diff --git a/src/ebus_sdk/declaration.py b/src/ebus_sdk/declaration.py index fbe5e45..99c6d39 100644 --- a/src/ebus_sdk/declaration.py +++ b/src/ebus_sdk/declaration.py @@ -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