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
+63 -7
View File
@@ -661,6 +661,12 @@ def create_app(config_object="config"):
device_locations = sorted({d.location for d in all_devices if d.location})
device_battery_sizes = sorted({d.battery_size for d in all_devices if d.battery_size})
# Pre-fill parent for "Add Sub-component" links
prefill_parent_id = request.args.get("parent_id", "").strip()
prefill_parent = None
if prefill_parent_id:
prefill_parent = db.get(Device, int(prefill_parent_id)) if prefill_parent_id.isdigit() else None
if request.method == "POST":
name = request.form.get("name", "").strip()
slots_raw = request.form.get("battery_slots", "1").strip()
@@ -669,12 +675,31 @@ def create_app(config_object="config"):
battery_size = request.form.get("battery_size", "").strip() or None
location = request.form.get("location", "").strip() or None
# Resolve parent device
parent_id_raw = request.form.get("parent_id", "").strip()
parent_device = None
if parent_id_raw and parent_id_raw.isdigit():
parent_device = db.get(Device, int(parent_id_raw))
if not parent_device:
flash("Parent device not found.", "error")
return render_template("device_add.html",
device_types=device_types,
device_locations=device_locations,
device_battery_sizes=device_battery_sizes), 400
if parent_device.is_subcomponent():
flash("Cannot nest sub-components more than one level deep.", "error")
return render_template("device_add.html",
device_types=device_types,
device_locations=device_locations,
device_battery_sizes=device_battery_sizes), 400
if not name:
flash("Device name is required.", "error")
return render_template("device_add.html",
device_types=device_types,
device_locations=device_locations,
device_battery_sizes=device_battery_sizes), 400
device_battery_sizes=device_battery_sizes,
prefill_parent=parent_device), 400
if not battery_size:
flash("Battery size is required.", "error")
@@ -683,7 +708,8 @@ def create_app(config_object="config"):
device_locations=device_locations,
device_battery_sizes=device_battery_sizes,
form_name=name, form_notes=notes or "",
form_device_type=request.form.get("device_type", "")), 400
form_device_type=request.form.get("device_type", ""),
prefill_parent=parent_device), 400
try:
slots = int(slots_raw)
@@ -696,7 +722,8 @@ def create_app(config_object="config"):
device_locations=device_locations,
device_battery_sizes=device_battery_sizes,
form_name=name, form_notes=notes or "",
form_device_type=request.form.get("device_type", "")), 400
form_device_type=request.form.get("device_type", ""),
prefill_parent=parent_device), 400
if db.query(Device).filter_by(name=name).first():
flash(f"A device named '{name}' already exists.", "error")
@@ -706,19 +733,24 @@ def create_app(config_object="config"):
device_battery_sizes=device_battery_sizes,
form_name=name, form_slots=slots,
form_notes=notes or "",
form_device_type=request.form.get("device_type", "")), 400
form_device_type=request.form.get("device_type", ""),
prefill_parent=parent_device), 400
device = Device(name=name, battery_slots=slots, notes=notes,
device_type=device_type, battery_size=battery_size,
location=location)
location=location,
parent_id=parent_device.id if parent_device else None)
db.add(device)
db.commit()
flash(f"Device '{name}' added.", "success")
if parent_device:
return redirect(url_for("device_detail", device_id=parent_device.id))
return redirect(url_for("device_list"))
return render_template("device_add.html", device_types=device_types,
device_locations=device_locations,
device_battery_sizes=device_battery_sizes)
device_battery_sizes=device_battery_sizes,
prefill_parent=prefill_parent)
# ------------------------------------------------------------------ #
# Devices — detail
@@ -767,6 +799,7 @@ def create_app(config_object="config"):
device_types=device_types,
device_locations=device_locations,
device_battery_sizes=device_battery_sizes,
device_list_all=all_devices,
ha_enabled=ha_client.enabled,
ha_live_pct=ha_live_pct,
logbook_entries=device.logbook_entries)
@@ -800,6 +833,23 @@ def create_app(config_object="config"):
flash(f"A device named '{name}' already exists.", "error")
return redirect(url_for("device_detail", device_id=device_id))
# Validate parent_id change
parent_id_raw = request.form.get("parent_id", "").strip()
if parent_id_raw and parent_id_raw.isdigit():
new_parent = db.get(Device, int(parent_id_raw))
if not new_parent:
flash("Parent device not found.", "error")
return redirect(url_for("device_detail", device_id=device_id))
if new_parent.id == device_id:
flash("A device cannot be its own parent.", "error")
return redirect(url_for("device_detail", device_id=device_id))
if new_parent.is_subcomponent():
flash("Cannot nest sub-components more than one level deep.", "error")
return redirect(url_for("device_detail", device_id=device_id))
device.parent_id = new_parent.id
else:
device.parent_id = None
device.name = name
device.battery_slots = slots
device.notes = notes
@@ -939,9 +989,15 @@ def create_app(config_object="config"):
battery.status = "available"
battery.device_id = None
name = device.name
child_names = [c.name for c in device.children]
for child in device.children:
child.parent_id = None
db.delete(device)
db.commit()
flash(f"Device '{name}' deleted. All batteries marked available.", "success")
msg = f"Device '{name}' deleted. All batteries marked available."
if child_names:
msg += f" Sub-components ({', '.join(child_names)}) are now independent devices."
flash(msg, "success")
return redirect(url_for("device_list"))
# ------------------------------------------------------------------ #