You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: key the builder on device id, and give extend() an inverse
#74: DeviceTreeBuilder keyed its bookkeeping on DeviceSpec object
identity. A producer deriving its spec set from a manifest re-derives
equal-but-distinct objects on every pass, so identity keying made each
pass a new device. The alternative was an unstated obligation: hold a
device_id -> DeviceSpec map for the process lifetime and never re-derive,
which is exactly what a declarative API exists to avoid.
Now keyed on the resolved device id. add(), remove(), extend(),
device_for() and homie_properties() all answer for any spec naming the
same device. Deferred specs stay keyed by identity, having no id yet by
definition.
One semantic decided explicitly: add() is idempotent on the DEVICE, not
on the declaration. A differing capability set on an already-built id
returns the existing device unchanged rather than applying the
difference, because add() silently mutating a live tree is not what its
name suggests. extend() is how a built device grows.
The test that asserted the old contract (device_for on an equal spec
returns None) is rewritten to assert the new one rather than deleted, so
the change of contract is visible in the diff.
#78: remove_capabilities(), the inverse of extend(). A capability that
becomes relevant at runtime can stop being relevant, and its node
otherwise stayed advertised with retained topics behind it.
Device.delete_node already clears those and re-announces; the gap was the
bookkeeping, since reaching around the builder left model_keys and
created_groups describing properties that no longer existed and a later
remove() working from that stale record. Bookkeeping now carries the
capability, so a node's share of it is identifiable.
Closes#74Closes#78
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+8Lines changed: 8 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,6 +4,14 @@ All notable changes to `ebus-sdk` are recorded here. Format follows [Keep a Chan
4
4
5
5
## [Unreleased]
6
6
7
+
### Added
8
+
9
+
-`DeviceTreeBuilder.remove_capabilities()`: the inverse of `extend()`. A capability that becomes relevant at runtime can stop being relevant, and without this its node stayed advertised in `$description` with retained topics behind it. `Device.delete_node()` already clears those and re-announces, so what this closes is the bookkeeping: reaching around the builder to call it left `model_keys` and `created_groups` describing properties that no longer existed, and a later `remove()` working from that stale record. Idempotent like `extend()`, and named for capabilities rather than nodes because that is the declarative vocabulary. ([#78](https://github.com/electrification-bus/python-sdk/issues/78))
10
+
11
+
### Changed
12
+
13
+
- `DeviceTreeBuilder` keys its bookkeeping on the **resolved device id** rather than on `DeviceSpec` object identity. A producer deriving its spec set from a manifest re-derives equal-but-distinct objects on every pass, and identity keying made each pass a new device; the alternative was an unstated obligation to hold a `device_id -> DeviceSpec` map for the process lifetime and never re-derive, which is precisely what a declarative API exists to avoid. `add()`, `remove()`, `extend()`, `device_for()` and `homie_properties()` now all answer for any spec naming the same device. `add()` remains idempotent on the DEVICE rather than on the declaration: a differing capability set on an already-built id returns the existing device unchanged rather than applying the difference, since `add()` mutating a live tree is not what its name suggests; `extend()` is how a built device grows. Deferred specs stay keyed by identity, having no id yet by definition. ([#74](https://github.com/electrification-bus/python-sdk/issues/74))
14
+
7
15
### Fixed
8
16
9
17
-`conditionally_settable` was inert: `_materialize` never read it. The half that looked right is that the property did come out not-settable; the half that bit is that the `entity_setter` was registered only when `settable` was true, so the caller's later `set_settable(True)` opened a `/set` topic with no translator behind it. The property then advertised that it accepts commands and silently discarded them, which is the exact failure the field was introduced to avoid, one step further along. It is the only route the API offers for per-instance settability decided at runtime, and it was the route that did not work. The translator is now wired at build time even though the property starts not-settable. The test that shipped with the feature asserted only the not-settable half, which is why this survived review: a test written from the design rationale checks the rationale rather than the feature. ([#72](https://github.com/electrification-bus/python-sdk/issues/72))
Copy file name to clipboardExpand all lines: doc/building-a-proxy.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -196,6 +196,8 @@ Four things the tree builder does that the single-device one has no need to:
196
196
-**`add()` is idempotent.** Incremental lifecycles re-fire, and a second `add()` of a built spec returns the same `Device` without republishing anything.
197
197
-**`remove()` is depth-first**, grandchild before parent, derived from the live tree rather than an ordering you maintain, so nothing ever observes an orphaned child. It also deletes the model entries it added, and any group it created that is now empty.
198
198
199
+
Specs are matched by the device id they resolve to, not by object identity, so you can re-derive your spec set from a manifest on every pass and `add()` / `extend()` / `remove()` keep answering for the same device. `add()` is idempotent on the device rather than the declaration: a differing capability set on a built id returns the existing device, and `extend(spec, specs)` / `remove_capabilities(spec, capabilities)` are how a built device grows and shrinks.
200
+
199
201
The root is not only a parent: `builder.add_root_capabilities(specs)` materializes capabilities onto the root device itself, keyed in the model by the root's device id, and `builder.extend(spec, specs)` gives an already-built device a capability it did not have at boot. Both are idempotent, and a call that would create nothing does not open a state transition at all, so a re-fired lifecycle costs no `init` to `ready` edge (an empty one still forces every controller on the bus to resync).
200
202
201
203
Each `add()` announces its own device and makes the parent republish its `$description`. To collapse a burst of adds into one parent announcement, wrap them in the parent's `state_transition()`. Use `on_created` for per-child side effects rather than post-processing the returned tree.
0 commit comments