Skip to content

Commit 42126ae

Browse files
dcjclaude
andcommitted
docs: add CHANGELOG, document parent/child trees in README
Start a CHANGELOG.md (Keep-a-Changelog format) anchored at 0.2.0. Entry documents the breaking changes — Device constructor signature, removed ID-based parent/child mutators, refresh_all_nodes rename — alongside the additive features: tree navigation, effective-state computation, delete protocol, reconnect cascade. Brief 0.1.7 and 0.1.2 entries reference git for pre-changelog history. README gains a "Device Trees" Quick Start section showing the parent= constructor pattern, batched state_transition, and 3-level depth. Controller Role section gains a hierarchy/effective-state example. Module Structure no longer mentions the removed mqtt.py (extracted to ebus-mqtt-client at 56ad978). homie.py contents list adds the new public surface: HOMIE_EFFECTIVE_STATE_TABLE, DiscoveredDevice hierarchy fields, Controller tree-nav methods. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a4152e9 commit 42126ae

2 files changed

Lines changed: 101 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Changelog
2+
3+
All notable changes to `ebus-sdk` are recorded here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Releases earlier than 0.2.0 predate this file — see `git log` and the corresponding `v0.1.x` tags for details.
4+
5+
## [Unreleased]
6+
7+
## [0.2.0] — 2026-06-11
8+
9+
The 0.2.0 release introduces first-class parent/child device trees on both the device (publisher) and controller (consumer) sides of the SDK. A panel-style root device can now own dozens of child devices that share a single MQTT connection and a single Last Will, and controllers can navigate the resulting tree and compute effective state per the Homie 5 spec without re-implementing the rules per consumer.
10+
11+
### Added
12+
13+
- `Device(parent=<Device>)` constructor argument for building child devices. The child borrows the root's MQTT connection — there is exactly one `MqttClient` per tree and exactly one Last Will registered, on the root's `$state` topic.
14+
- `Device.root()`, `Device.parent()`, `Device.children()` — live references that walk the tree. `root_id()` / `parent_id()` accessors are now derived from these. Trees may be arbitrary depth (e.g. panel → BESS child → MID grandchild).
15+
- `Device.delete()` — runs the Homie remove-child protocol (clear retained `$state` / `$description` / property values, detach from parent, re-publish parent's description). Recursive on roots: leaves-first teardown of the whole subtree.
16+
- `Device.refresh_tree()` — recursive republish of description + nodes + state for every device under this one. Called automatically by `on_connect()` on broker reconnect so the entire tree's retained-state is re-established.
17+
- `DiscoveredDevice.root_id`, `parent_id`, `children_ids`, `is_root` — hierarchy fields derived from `$description`.
18+
- `Controller.get_root_devices()`, `get_root(device_id)`, `get_children(device_id)`, `get_descendants(device_id)` — tree-navigation API.
19+
- `Controller.get_effective_state(device_id)` and the public `HOMIE_EFFECTIVE_STATE_TABLE` module constant — implement the Homie 5 state-precedence rule. When a root is `init` / `disconnected` / `sleeping` / `lost`, every descendant is effectively the same state without each descendant needing to republish.
20+
21+
### Changed
22+
23+
- **BREAKING**`Device` constructor signature. The previous string-ID args `root_id=`, `parent_id=`, and `children_ids=` are removed; use `parent=<Device>` instead. Pure root-device usage (`Device(id, name, type, mqtt_cfg, ...)`) is unchanged and source-compatible.
24+
- The `Device._end_state_transition()` → broker sequence now emits exactly one `$state=init` and one `$state=ready` regardless of nesting depth (state transitions are reentrant via an internal depth counter). Previously a nested `with device.state_transition(): with device.state_transition(): ...` emitted a spurious extra INIT/READY cycle from the outer `__exit__`, forcing every controller in the wild to resync unnecessarily.
25+
- `add_node()` and other structural mutations inside `with parent.state_transition(): ...` now collapse into a single parent `$state=init``$description``$state=ready` cycle for the whole batch. Building a panel with 32 circuit children produces one observable parent transition, not 32.
26+
- `Device.publish()`, `delete_all_from_mqtt()`, `clear_retained_topic()`, and the `Property``Node``Device` publish chain all route through `Device.get_mqtt_client()`, which ascends to `root().mqttc`. There is no behavioral change for root devices; child devices now publish their topics through the root's connection automatically.
27+
28+
### Removed
29+
30+
- **BREAKING**`Device.add_child(child_id)`. Children are added by constructing them with `parent=<Device>`; the `_children` list is maintained automatically.
31+
- **BREAKING**`Device.remove_child(child_id)`. Use `child_device.delete()` instead — it runs the full Homie remove-child protocol rather than just popping an ID from a list.
32+
- **BREAKING**`Device.set_parent(parent_id)` and `Device.unset_parent()`. Homie does not support reparenting at the wire level; destroy and reconstruct instead.
33+
- **BREAKING**`Device.refresh_all_nodes()`. Renamed to `refresh_tree()` to reflect that it now walks descendants. The old name was misleading: it always published `$description` and `$state` in addition to nodes.
34+
35+
### Fixed
36+
37+
- Recursive `Device.delete()` no longer emits gratuitous `$state=init` / `$state=ready` publishes on dying intermediate devices while the cascade is in progress.
38+
- Broker reconnect now republishes every device in the tree (description, nodes, property values, state), not just the root.
39+
40+
## [0.1.7] — 2025
41+
42+
### Fixed
43+
44+
- Added `setup.py` shim for legacy setuptools toolchains that can't read `pyproject.toml`-only packages.
45+
46+
## [0.1.2] — 2025
47+
48+
Initial public release on PyPI. See `git log v0.1.2` for the surface that shipped.
49+
50+
[Unreleased]: https://github.com/electrification-bus/python-sdk/compare/v0.2.0...HEAD
51+
[0.2.0]: https://github.com/electrification-bus/python-sdk/releases/tag/v0.2.0
52+
[0.1.7]: https://github.com/electrification-bus/python-sdk/releases/tag/v0.1.7
53+
[0.1.2]: https://github.com/electrification-bus/python-sdk/releases/tag/v0.1.2

README.md

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,30 @@ device.start_mqtt_client()
4646
temp.set_value(23.5)
4747
```
4848

49+
### Device Trees (parent / child)
50+
51+
Build a tree of devices that share a single MQTT connection. The root device owns the connection (and the Last Will), every child borrows it via the `parent=` constructor arg, and `$description` `root` / `parent` / `children` fields are kept in sync automatically. The tree can be any depth.
52+
53+
```python
54+
panel = Device('panel-1', type='energy.ebus.device.electrical-panel', mqtt_cfg={...})
55+
panel.start_mqtt_client()
56+
57+
# Add 32 circuit children inside one state transition — the broker sees
58+
# exactly one INIT→READY cycle on the panel, not 32.
59+
with panel.state_transition():
60+
for cid in commissioned_circuits:
61+
Device(id=cid, type='energy.ebus.device.circuit', parent=panel)
62+
63+
# Three-level tree: panel → BESS child → MID grandchild
64+
bess = Device(id='bess-1', type='...battery-storage', parent=panel)
65+
Device(id='mid-1', type='...metering', parent=bess)
66+
67+
# Remove a child at runtime (runs the Homie remove-child protocol)
68+
panel.children()[0].delete()
69+
```
70+
71+
Children may have children of their own. A single Last Will registered on the root marks the entire tree `lost` if the publisher process dies — controllers compute effective state per the Homie 5 precedence table (see [`HOMIE_EFFECTIVE_STATE_TABLE`](src/ebus_sdk/homie.py)).
72+
4973
### Controller Role
5074

5175
Discover and monitor Homie devices:
@@ -65,33 +89,43 @@ controller.set_on_property_changed_callback(on_property_changed)
6589
controller.start_discovery()
6690
```
6791

92+
Controllers can also navigate device hierarchies and compute effective state:
93+
94+
```python
95+
# Walk the tree
96+
roots = controller.get_root_devices()
97+
for root in roots:
98+
for descendant in controller.get_descendants(root.device_id):
99+
# When the root is lost/disconnected/sleeping/init, every descendant
100+
# is effectively the same regardless of its own reported $state.
101+
print(f'{descendant.device_id}: {controller.get_effective_state(descendant.device_id)}')
102+
```
103+
68104
## Module Structure
69105

70106
```
71107
src/ebus_sdk/
72108
├── __init__.py # Package exports
73-
├── homie.py # Homie convention implementation
74-
├── mqtt.py # MQTT client wrapper
75-
└── property.py # Property abstractions
109+
├── homie.py # Homie convention implementation (Device, Node, Property, Controller, ...)
110+
└── property.py # Application-level property abstractions
76111
```
77112

113+
MQTT transport lives in the separate [`ebus-mqtt-client`](https://github.com/electrification-bus/ebus-mqtt-client) package; this SDK depends on it.
114+
78115
### homie.py
79116

80117
Core Homie convention implementation:
81118

82-
- **Device** - Represents a Homie device with nodes and properties
119+
- **Device** - Represents a Homie device; pass `parent=` to build a child in a tree
83120
- **Node** - Groups related properties within a device
84121
- **Property** - Individual data points (sensors, controls)
85-
- **Controller** - Discovers and monitors Homie devices on a broker
86-
- **DiscoveredDevice** - Represents a device found by the controller
122+
- **Controller** - Discovers and monitors Homie devices on a broker; navigates trees and computes effective state
123+
- **DiscoveredDevice** - Represents a device found by the controller; exposes `root_id`, `parent_id`, `children_ids`, `is_root`
87124
- **DeviceState** - Enum: `init`, `ready`, `disconnected`, `sleeping`, `lost`
125+
- **HOMIE_EFFECTIVE_STATE_TABLE** - Homie 5 state-precedence table used by `Controller.get_effective_state()`
88126
- **PropertyDatatype** - Enum: `STRING`, `INTEGER`, `FLOAT`, `BOOLEAN`, `ENUM`, `COLOR`, `DATETIME`, `DURATION`, `JSON`
89127
- **Unit** - Common units: `DEGREE_CELSIUS`, `PERCENT`, `WATT`, `KILOWATT_HOUR`, etc.
90128

91-
### mqtt.py
92-
93-
- **MqttClient** - Wrapper around paho-mqtt with automatic reconnection, TLS support, and subscription management
94-
95129
### property.py
96130

97131
Application-level property abstractions for bridging application state to Homie:
@@ -110,6 +144,10 @@ See [`examples/README.md`](examples/README.md) for example scripts demonstrating
110144
- Python 3.10+
111145
- paho-mqtt >= 1.6.1
112146

147+
## Releases
148+
149+
See [CHANGELOG.md](CHANGELOG.md). 0.2.0 introduces parent/child device trees and contains breaking changes to the `Device` constructor — see the changelog entry before upgrading from 0.1.x.
150+
113151
## Contributing
114152

115153
See [CONTRIBUTING.md](CONTRIBUTING.md) for how to file Discussions, Issues, and pull requests. Pure MQTT-transport changes (TLS, auth, paho upgrades) belong in [`ebus-mqtt-client`](https://github.com/electrification-bus/ebus-mqtt-client), not here. Normative behavior tracks the [Electrification Bus specification](https://github.com/electrification-bus/specification).

0 commit comments

Comments
 (0)