a46e505505
The previous parser read `data.routes` which is just an echo of the
`routes` request flag (a boolean). On real brokers this caused
`TypeError: 'bool' object is not iterable` and 500s during /import.
- Rewrite parse_networkmap to read data.value.nodes + data.value.links
with fallback to data.{nodes,links} for legacy variants
- Defensive: drop links to unknown nodes, propagate lqi from link to
target node, extract model/vendor from definition block
- Bump networkmap timeout 10s -> 180s (large meshes are slow)
- Tests: rewrite fixture builders + sample payload to real Z2M shape;
add cases for legacy shape, routes:false echo (regression), malformed
list, link to unknown node, lqi propagation, definition extraction
- Update docs to mention 60s+ wait window
53 backend tests pass, mypy + ruff clean.
84 lines
3.3 KiB
Python
84 lines
3.3 KiB
Python
"""FastAPI router for Zigbee2MQTT import."""
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
from app.api.deps import get_current_user
|
|
from app.schemas.zigbee import (
|
|
ZigbeeEdgeOut,
|
|
ZigbeeImportRequest,
|
|
ZigbeeImportResponse,
|
|
ZigbeeNodeOut,
|
|
ZigbeeTestConnectionRequest,
|
|
ZigbeeTestConnectionResponse,
|
|
)
|
|
from app.services.zigbee_service import fetch_networkmap, test_mqtt_connection
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/import", response_model=ZigbeeImportResponse)
|
|
async def import_zigbee_network(
|
|
payload: ZigbeeImportRequest,
|
|
_: str = Depends(get_current_user),
|
|
) -> ZigbeeImportResponse:
|
|
"""Fetch the Zigbee2MQTT network map and return nodes + edges ready for canvas drop.
|
|
|
|
Connects to the specified MQTT broker, publishes a networkmap request to
|
|
``<base_topic>/bridge/request/networkmap``, and waits up to 60 s for the
|
|
response (large meshes can take 30 s+). The devices are returned as typed homelable nodes with a
|
|
coordinator → router → end-device hierarchy.
|
|
"""
|
|
try:
|
|
nodes_raw, edges_raw = await fetch_networkmap(
|
|
mqtt_host=payload.mqtt_host,
|
|
mqtt_port=payload.mqtt_port,
|
|
base_topic=payload.base_topic,
|
|
username=payload.mqtt_username,
|
|
password=payload.mqtt_password,
|
|
tls=payload.mqtt_tls,
|
|
tls_insecure=payload.mqtt_tls_insecure,
|
|
)
|
|
except ImportError as exc:
|
|
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
except ConnectionError as exc:
|
|
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
|
except TimeoutError as exc:
|
|
raise HTTPException(status_code=504, detail=str(exc)) from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
except Exception as exc:
|
|
logger.exception("Unexpected error during Zigbee import")
|
|
raise HTTPException(status_code=500, detail="Unexpected error during Zigbee import") from exc
|
|
|
|
nodes = [ZigbeeNodeOut(**n) for n in nodes_raw]
|
|
edges = [ZigbeeEdgeOut(**e) for e in edges_raw]
|
|
return ZigbeeImportResponse(nodes=nodes, edges=edges, device_count=len(nodes))
|
|
|
|
|
|
@router.post("/test-connection", response_model=ZigbeeTestConnectionResponse)
|
|
async def test_zigbee_connection(
|
|
payload: ZigbeeTestConnectionRequest,
|
|
_: str = Depends(get_current_user),
|
|
) -> ZigbeeTestConnectionResponse:
|
|
"""Quick MQTT ping to validate broker connection before importing."""
|
|
try:
|
|
await test_mqtt_connection(
|
|
mqtt_host=payload.mqtt_host,
|
|
mqtt_port=payload.mqtt_port,
|
|
username=payload.mqtt_username,
|
|
password=payload.mqtt_password,
|
|
tls=payload.mqtt_tls,
|
|
tls_insecure=payload.mqtt_tls_insecure,
|
|
)
|
|
return ZigbeeTestConnectionResponse(connected=True, message="Connection successful")
|
|
except ImportError as exc:
|
|
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
except (ConnectionError, TimeoutError) as exc:
|
|
return ZigbeeTestConnectionResponse(connected=False, message=str(exc))
|
|
except Exception:
|
|
logger.exception("Unexpected error during connection test")
|
|
return ZigbeeTestConnectionResponse(connected=False, message="Unexpected error")
|