Add sub-component support via self-referential device hierarchy

This commit is contained in:
2026-06-05 22:30:11 -05:00
parent 55ce963d79
commit 75fe4fe575
7 changed files with 248 additions and 9 deletions
+96
View File
@@ -885,3 +885,99 @@ def test_device_charge_all_no_installed(seeded_client):
)
assert resp.status_code == 200
assert b"No installed batteries" in resp.data
# ------------------------------------------------------------------ #
# Sub-components (self-referential device hierarchy)
# ------------------------------------------------------------------ #
def _setup_rc_car(client):
"""Create RC Car Set with Remote and Car sub-components and 6 AA batteries."""
client.post("/device/add", data={"name": "RC Car Set", "battery_slots": "6", "battery_size": "AA"})
# id=1 above; add sub-components with parent_id=1
client.post("/device/add", data={"name": "Remote", "battery_slots": "2",
"battery_size": "AA", "parent_id": "1"})
client.post("/device/add", data={"name": "Car", "battery_slots": "4",
"battery_size": "AA", "parent_id": "1"})
for i in range(1, 7):
client.post("/battery/add", data={"brand": "Eneloop", "label": f"E{i:03d}", "size": "AA"})
def test_add_subcomponent(client):
_setup_rc_car(client)
# Parent detail shows sub-components
resp = client.get("/device/1")
assert resp.status_code == 200
assert b"Remote" in resp.data
assert b"Car" in resp.data
# Child detail shows breadcrumb back to parent
resp = client.get("/device/2")
assert resp.status_code == 200
assert b"RC Car Set" in resp.data
assert b"Remote" in resp.data
def test_subcomponent_prevents_deep_nesting(client):
_setup_rc_car(client)
# id=2 is "Remote", which already has parent_id=1
resp = client.post("/device/add", data={"name": "Button", "battery_slots": "1",
"battery_size": "AA", "parent_id": "2"})
assert resp.status_code == 400
assert b"one level" in resp.data.lower() or b"nest" in resp.data.lower()
def test_subcomponent_install_batteries(client):
_setup_rc_car(client)
# Install 2 batteries into Remote (device id=2)
resp = client.post("/device/2/install-one", data={"battery_id": "1"}, follow_redirects=True)
assert resp.status_code == 200
resp = client.post("/device/2/install-one", data={"battery_id": "2"}, follow_redirects=True)
assert resp.status_code == 200
# Remote is now full (2/2); a third battery should be blocked
resp = client.post("/device/2/install-one", data={"battery_id": "3"}, follow_redirects=True)
assert resp.status_code == 200
assert b"full" in resp.data.lower()
def test_dashboard_shows_parent_slash_child(client):
_setup_rc_car(client)
client.post("/device/2/install-one", data={"battery_id": "1"})
resp = client.get("/", follow_redirects=True)
assert resp.status_code == 200
assert b"RC Car Set" in resp.data
assert b"Remote" in resp.data
def test_delete_parent_promotes_children(client):
_setup_rc_car(client)
client.post("/device/1/delete", follow_redirects=True)
# Child devices still exist
resp = client.get("/device/2")
assert resp.status_code == 200
resp = client.get("/device/3")
assert resp.status_code == 200
def test_device_without_parent_unchanged(client):
client.post("/device/add", data={"name": "Torch", "battery_slots": "2", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
resp = client.post("/device/1/install-one", data={"battery_id": "1"}, follow_redirects=True)
assert resp.status_code == 200
assert b"1 / 2" in resp.data
def test_subcomponent_ha_entity_id(client):
client.post("/device/add", data={"name": "Hub", "battery_slots": "1", "battery_size": "AA"})
resp = client.post(
"/device/add",
data={"name": "Sensor A", "battery_slots": "1", "battery_size": "AA",
"parent_id": "1", "ha_entity_id": "sensor.sensor_a_battery"},
follow_redirects=True,
)
# Redirects to parent detail on success
assert resp.status_code == 200
assert b"Sensor A" in resp.data
# Child detail shows parent breadcrumb
resp = client.get("/device/2")
assert resp.status_code == 200
assert b"Hub" in resp.data