Saltar a contenido

Protocolo Analytics Events (puerto 9090) — el contrato real

Fase 0 completada. Verificado en vivo contra XProtect VMS 2026 R1 el 2026-09-20. El contrato se extrajo de XProtect Event Server\MIPPlugins\VideoOS.Analytics.Events.Service\VideoOS.Analytics.Events.Service.dll porque la documentación no alcanza para hacerlo andar.

Resultado

2026-09-21 02:58:22 >> Intrusion - Porton Norte (CAM-01 zona 2) >> tipo=Hanwha Intrusion_2

Evento inyectado → aceptado → asociado a la cámara correcta → dispara la definición de alarma filtrada por tipo + cámara. La discriminación por zona funciona de extremo a extremo.

Prueba de control: se enviaron Hanwha Intrusion_1 y Hanwha Intrusion_2. Ambos aceptados, pero sólo el _2 generó alarma, porque sólo el _2 tiene definición de alarma. Es exactamente el comportamiento buscado.


Las tres trampas

El servidor siempre responde HTTP/1.1 200 OK, incluso cuando descarta el evento. El rechazo viene en el cuerpo de la respuesta. Si se manda y se cierra el socket sin leer, todo parece funcionar y no llega nada. Costó varias vueltas.

1. El namespace es obligatorio

<AnalyticsEvent xmlns="urn:milestone-systems">

El servidor busca con XPath:

/ns:AnalyticsEvent/ns:EventHeader/ns:Message

Sin el namespace el XPath no matchea, Message queda nulo y responde Warning: Event message not known.

2. El nombre del tipo va en <Message>, no en <Type>

Contraintuitivo pero es así. <Message> debe ser exactamente el nombre del Analytics Event definido en Management Client → Reglas y eventos → Eventos analíticos. <Type> es texto libre y el servidor lo ignora para el matcheo.

3. El origen se resuelve por IP o FQID, nunca por nombre de cámara

<Source> Resultado
<Name>CAM-01</Name> Warning: Device not known
<Name>Hanwha Vision TNO-4050T (192.0.2.10)</Name> Warning: Device not known
<Name>192.0.2.10</Name> ✅ aceptado
<FQID><ObjectId>{guid}</ObjectId></FQID> ✅ aceptado (preferido)

El componente se llama HostResolver.cs: resuelve por host, no por nombre amigable.

Para el servicio conviene el FQID, porque identifica el canal exacto — con la IP, un hardware multicanal es ambiguo. El GUID sale del MIP SDK ((Get-VmsCamera).Id), que es de donde ya sacamos el inventario.


XML que funciona

<?xml version="1.0" encoding="utf-8"?>
<AnalyticsEvent xmlns="urn:milestone-systems">
  <EventHeader>
    <ID>3f2504e0-4f89-11d3-9a0c-0305e82c3301</ID>
    <Timestamp>2026-09-21T02:58:22.903Z</Timestamp>
    <Type>Analytics</Type>
    <Message>Hanwha Intrusion_2</Message>
    <CustomTag>HanwhaZoneBridge</CustomTag>
    <Source>
      <FQID><ObjectId>89d3bfd1-f5c8-4f6d-8805-82a510570cf9</ObjectId></FQID>
    </Source>
  </EventHeader>
  <Description>slot 2 = area 2 = Porton Norte</Description>
  <Location>lab</Location>
  <Vendor><Name>HanwhaZoneBridge</Name></Vendor>
</AnalyticsEvent>

Se manda por socket TCP plano al 9090 y se lee la respuesta.

Respuestas del servidor

Cuerpo Significado
(vacío) ✅ aceptado
Warning: Event message not known El <Message> no matchea ningún Analytics Event
Warning: Device not known No pudo resolver el origen
Error: Invalid data XML mal formado (también responde 400)

El servicio debe tratar cualquier cuerpo no vacío como fallo y loguearlo. Son exactamente los dos errores que se van a ver en producción: tipo mal escrito, o cámara que ya no existe.


Configuración del lado Milestone

Verificado en Herramientas → Opciones → Eventos analíticos:

  • Habilitado: sí
  • Puerto: 9090
  • Seguridad → Eventos permitidos de: Todas las direcciones de red

Si se pasa a Direcciones de red especificadas, hay que agregar la IP del host donde corra HanwhaZoneBridge. Como el servicio va a correr en el mismo servidor, conviene dejarlo así o listar explícitamente esa IP.

El listener no necesita reinicio del Event Server para tomar tipos nuevos. Se probó reiniciando el servicio y el comportamiento no cambió: el problema era el formato, no un caché. Tipos creados por Configuration API quedan utilizables enseguida.

Crear los Analytics Event Types por código

No hay cmdlet dedicado en MilestonePSTools; va por Configuration API:

$inv = Get-ConfigurationItem -Path "/AnalyticsEventFolder" |
       Invoke-Method -MethodId AddAnalyticsEvent
($inv.Properties | Where-Object Key -eq 'Name').Value        = 'Hanwha Intrusion_2'
($inv.Properties | Where-Object Key -eq 'Description').Value = 'Generado por HanwhaZoneBridge'
$inv | Invoke-Method -MethodId AddAnalyticsEvent

Y la definición de alarma:

$inv = Get-ConfigurationItem -Path "/AlarmDefinitionFolder" |
       Invoke-Method -MethodId AddAlarmDefinition
($inv.Properties | Where-Object Key -eq 'EventTypeGroup').Value = 'a96692c8-51b1-4f87-b12c-0d3d9cbfc5a4'  # Analytics Events
($inv.Properties | Where-Object Key -eq 'EventType').Value      = '<guid del analytics event>'
($inv.Properties | Where-Object Key -eq 'SourceList').Value     = (Get-VmsCamera | ? Name -eq 'CAM-01').Path
($inv.Properties | Where-Object Key -eq 'Name').Value           = 'Intrusion - Porton Norte'
$inv | Invoke-Method -MethodId AddAlarmDefinition

El GUID del grupo Analytics Events es constante: a96692c8-51b1-4f87-b12c-0d3d9cbfc5a4.

Ojo: EventTypeGroup y EventType hay que setearlos juntos antes de reinvocar. Si se setea sólo el grupo, falla con "Triggering event type is out of range".


Qué quedó creado en el sistema durante la prueba

Para limpiar o reutilizar:

  • Analytics Event Types: Hanwha Intrusion_1, Hanwha Intrusion_2
  • Alarm Definition: Intrusion - Porton Norte (CAM-01 zona 2)
  • Script en el escritorio: enviar_analytics_event.ps1

Conviene dejarlos: son la base de la Fase 1 y sirven de referencia viva.