feat(devices): wifi tcp registry, device API/UI, tests; bump led-tool

Made-with: Cursor
This commit is contained in:
pi
2026-04-05 21:13:07 +12:00
parent fbae75b957
commit e6b5bf2cf1
15 changed files with 825 additions and 103 deletions

View File

@@ -1,5 +1,10 @@
from microdot import Microdot
from models.device import Device
from models.device import (
Device,
derive_device_mac,
validate_device_transport,
validate_device_type,
)
import json
controller = Microdot()
@@ -23,7 +28,9 @@ async def get_device(request, id):
dev = devices.read(id)
if dev:
return json.dumps(dev), 200, {"Content-Type": "application/json"}
return json.dumps({"error": "Device not found"}), 404
return json.dumps({"error": "Device not found"}), 404, {
"Content-Type": "application/json",
}
@controller.post("")
@@ -32,37 +39,91 @@ async def create_device(request):
try:
data = request.json or {}
name = data.get("name", "").strip()
if not name:
return json.dumps({"error": "name is required"}), 400, {
"Content-Type": "application/json",
}
try:
device_type = validate_device_type(data.get("type", "led"))
transport = validate_device_transport(data.get("transport", "espnow"))
except ValueError as e:
return json.dumps({"error": str(e)}), 400, {
"Content-Type": "application/json",
}
address = data.get("address")
mac = data.get("mac")
if derive_device_mac(mac=mac, address=address, transport=transport) is None:
return json.dumps(
{
"error": "mac is required (12 hex digits); for Wi-Fi include mac plus IP in address"
}
), 400, {"Content-Type": "application/json"}
default_pattern = data.get("default_pattern")
tabs = data.get("tabs")
if isinstance(tabs, list):
tabs = [str(t) for t in tabs]
else:
tabs = []
dev_id = devices.create(name=name, address=address, default_pattern=default_pattern, tabs=tabs)
dev_id = devices.create(
name=name,
address=address,
mac=mac,
default_pattern=default_pattern,
tabs=tabs,
device_type=device_type,
transport=transport,
)
dev = devices.read(dev_id)
return json.dumps({dev_id: dev}), 201, {"Content-Type": "application/json"}
except ValueError as e:
msg = str(e)
code = 409 if "already exists" in msg.lower() else 400
return json.dumps({"error": msg}), code, {"Content-Type": "application/json"}
except Exception as e:
return json.dumps({"error": str(e)}), 400
return json.dumps({"error": str(e)}), 400, {"Content-Type": "application/json"}
@controller.put("/<id>")
async def update_device(request, id):
"""Update a device."""
try:
data = request.json or {}
raw = request.json or {}
data = dict(raw)
data.pop("id", None)
data.pop("addresses", None)
if "name" in data:
n = (data.get("name") or "").strip()
if not n:
return json.dumps({"error": "name cannot be empty"}), 400, {
"Content-Type": "application/json",
}
data["name"] = n
if "type" in data:
data["type"] = validate_device_type(data.get("type"))
if "transport" in data:
data["transport"] = validate_device_transport(data.get("transport"))
if "tabs" in data and isinstance(data["tabs"], list):
data["tabs"] = [str(t) for t in data["tabs"]]
if devices.update(id, data):
return json.dumps(devices.read(id)), 200, {"Content-Type": "application/json"}
return json.dumps({"error": "Device not found"}), 404
return json.dumps({"error": "Device not found"}), 404, {
"Content-Type": "application/json",
}
except ValueError as e:
return json.dumps({"error": str(e)}), 400, {"Content-Type": "application/json"}
except Exception as e:
return json.dumps({"error": str(e)}), 400
return json.dumps({"error": str(e)}), 400, {"Content-Type": "application/json"}
@controller.delete("/<id>")
async def delete_device(request, id):
"""Delete a device."""
if devices.delete(id):
return json.dumps({"message": "Device deleted successfully"}), 200
return json.dumps({"error": "Device not found"}), 404
return (
json.dumps({"message": "Device deleted successfully"}),
200,
{"Content-Type": "application/json"},
)
return json.dumps({"error": "Device not found"}), 404, {
"Content-Type": "application/json",
}