Files
homelable/backend/app/schemas/scan.py
T
Pouzor abefc42fdf fix: tolerate legacy NULL properties on pending devices
The pending_devices.properties column is added by an idempotent migration, so
existing rows have properties = NULL. PendingDeviceResponse typed it as a list,
so GET /scan/pending 500'd on any pre-existing device.

- Coerce NULL/non-list properties to [] in PendingDeviceResponse.
- Backfill existing NULL rows to '[]' in init_db migrations.
- Regression test: /scan/pending returns 200 with a legacy NULL-properties row.

ha-relevant: maybe
2026-07-06 00:01:27 +02:00

58 lines
1.9 KiB
Python

from datetime import datetime
from typing import Any
from pydantic import BaseModel, field_validator
class PendingDeviceResponse(BaseModel):
id: str
ip: str | None
mac: str | None
hostname: str | None
os: str | None
services: list[Any]
suggested_type: str | None
status: str
discovery_source: str | None
ieee_address: str | None = None
friendly_name: str | None = None
device_subtype: str | None = None
model: str | None = None
vendor: str | None = None
lqi: int | None = None
# Display properties carried from discovery (e.g. Proxmox specs). Merged into
# the node on approve; empty for scan/mesh sources that don't set them.
properties: list[Any] = []
discovered_at: datetime
# Number of distinct canvases (designs) this device already appears on,
# correlated by ip / ieee_address against existing nodes. Computed per-request.
canvas_count: int = 0
# Timestamps from the linked canvas node(s), correlated by ip / ieee_address.
# Null when the device is not on any canvas yet. Aggregated across matches:
# created_at = oldest; last_scan / last_modified / last_seen = newest.
node_created_at: datetime | None = None
node_last_scan: datetime | None = None
node_last_modified: datetime | None = None
node_last_seen: datetime | None = None
@field_validator("properties", mode="before")
@classmethod
def _coerce_properties(cls, v: Any) -> list[Any]:
# Legacy rows (column added by migration) have properties = NULL.
return v if isinstance(v, list) else []
model_config = {"from_attributes": True}
class ScanRunResponse(BaseModel):
id: str
status: str
kind: str = "ip"
ranges: list[str]
devices_found: int
started_at: datetime
finished_at: datetime | None
error: str | None
model_config = {"from_attributes": True}