Contexto
Un NVR Hikvision puede tener 4, 8, 16 o 32 camaras conectadas. No hay un boton de "listar camaras" en la API. Hay que consultar el endpoint correcto y parsear XML con quirks de formato.
Lo que aprendi
El endpoint /ISAPI/ContentMgmt/InputProxy/channels retorna las camaras IP conectadas al NVR:
async def list_cameras(self) -> list[dict]:
"""Descubre camaras conectadas al NVR via ISAPI."""
data = await self.client.get_xml(
"/ISAPI/ContentMgmt/InputProxy/channels"
)
channels = data.get("InputProxyChannelList", {}).get(
"InputProxyChannel", []
)
# QUIRK: si solo hay 1 camara, la respuesta es dict, no list
if isinstance(channels, dict):
channels = [channels]
cameras = []
for ch in channels:
source = ch.get("sourceInputPortDescriptor", {})
cameras.append({
"channel_id": int(ch.get("id", 0)),
"name": ch.get("name", f"Camera {ch.get('id')}"),
"ip_address": source.get("ipAddress"),
"is_online": ch.get("online", "false").lower() == "true",
"model": source.get("model"),
"serial": source.get("serialNumber"),
})
return cameras
Dos trampas importantes:
-
Una sola camara = dict, multiples = list. Si no normalizas con
isinstance(channels, dict), tu codigo falla en NVRs con una sola camara. -
El campo
onlinees string"true"/"false", no booleano. Requiere.lower() == "true".
El descubrimiento completo combina tres endpoints:
async def discover_all(self) -> dict:
nvr_info = await self.get_device_info() # /ISAPI/System/deviceInfo
cameras = await self.list_cameras() # /ISAPI/ContentMgmt/InputProxy/channels
streaming = await self.list_streaming_channels() # /ISAPI/Streaming/channels
return {
"nvr": nvr_info,
"cameras": cameras,
"streaming_channels": streaming,
"camera_count": len(cameras),
"online_count": sum(1 for c in cameras if c["is_online"]),
}
Por que importa
Con este patron, tu sistema detecta automaticamente que camaras hay sin configuracion manual. Cuando alguien agrega o quita una camara del NVR, el proximo discovery lo refleja.