Cámara: poder controlar las cámaras compatibles con ONVIF

La integración de la cámara permite hoy en día mostrar en el panel la imagen de una cámara que publique un flujo RTSP y activar el flujo de video. También se puede enviar la imagen de una cámara a Telegram desde una escena. Pero se podría hacer más :wink:

Cuando una cámara es compatible con ONVIF (por ejemplo, es el caso de las cámaras TP-Link Tapo), es técnicamente posible, a través de este protocolo estandarizado, controlar la cámara. Ver explicaciones aquí.

Creo que sería útil en Gladys poder acceder a las siguientes funciones desde una escena o el panel:

  • activar/desactivar una cámara
  • modificar la orientación « horizontal » de la cámara (pan)
  • modificar la orientación « vertical » de la cámara (tilt)
  • modificar el zoom de la cámara (zoom)
  • (y si es posible) posicionar la cámara en uno de sus preajustes (pan+tilt+zoom)
  • gestionar la detección de movimiento de la cámara (para usarla como disparador de escena)
  • Transmitir un texto en « text-to-speech » en una cámara (haciendo seleccionable una cámara en la acción « hablar en un altavoz »).

Algunos ejemplos de casos de uso:

  • cuando salgo de casa, activa las cámaras (y viceversa cuando vuelvo)
  • cada noche, cuando estoy ausente, envíame a Telegram la foto de mi cámara mirando sucesivamente diferentes ángulos de la habitación
  • en caso de intrusión, transmite un mensaje disuasorio en mi cámara
  • …

De lo que entiendo de la norma ONVIF, es el « Perfil S » el que hay que tener en cuenta. Los perfiles ONVIF están descritos aquí.

No sé si esto puede ayudar al desarrollo, pero hay un plugin de Home Assistant disponible en Github que gestiona el protocolo ONVIF para las cámaras Tapo: aquí

¿Quién nos hace una pequeña integración externa ONVIF? :fire:

Puedo mirar ya para TAPO habiendo hecho una integración externa TAPO

He lanzado a Claude y ya ha terminado. Lo probaré esta noche y os diré si funciona. Mientras tanto, tengo mucho calor, así que ¡pausa piscina para mí!

Beneficios :smiley: Eso sí, los dos no son incompatibles, con Claude Code en móvil :joy:

El móvil en el agua un poco menos :joy:

@pierre-gilles Una propuesta de claude para modificar el sdk y el núcleo para la gestión de cámaras ONVIF, PTZ…

Propuesta: control PTZ de cámaras en Gladys

Este documento propone la adición del control PTZ (pan / tilt / zoom) de las cámaras a Gladys:
las constantes a agregar al SDK, la manera en que un comando llega a la integración,
y un widget de control en el panel de control.

Está escrito desde una integración externa existente — gladys-tapo —
que ya habla ONVIF a cámaras TP-Link Tapo y para las cuales el PTZ está medido como
disponible del lado de la cámara, pero inexpresable del lado de Gladys.


1. La necesidad

Una cámara motorizada (Tapo C210, C500, TC70, y la mayoría de las cámaras ONVIF del mercado)
sabe hacer tres cosas que ninguna categoría de Gladys cubre hoy:

  • moverse en una dirección, más o menos rápido y más o menos lejos;
  • unirse a una posición registrada (« entrada », « jardín »);
  • detenerse.

Los usos domóticos correspondientes son clásicos:

  • « cuando se toca el timbre, la cámara del salón mira la entrada »;
  • « de noche, la cámara se gira hacia la puerta; por la mañana, vuelve »;
  • controlar la cámara manualmente desde el panel de control, sin abrir la app del fabricante.

Lo que bloquea hoy

DEVICE_FEATURE_TYPES.CAMERA solo contiene una entrada:

CAMERA: {
  IMAGE: 'image',
},

Una cámara Gladys es, por construcción, una fuente de imágenes de solo lectura.
Ninguna categoría existente es adecuada para sortear:

Ruta considerada Por qué se descarta
CURTAIN.POSITION para el pan Muestra una cortina en una cámara; semánticamente falso, y bloquea la adición de un PTZ real más tarde
SWITCH.BINARY por dirección Cuatro interruptores para una cruz direccional; no lleva ni velocidad ni distancia
Acciones del manifiesto Funciona, pero las acciones no son utilizables en una escena — y ese es el uso principal

El objeto de esta propuesta es, por lo tanto, agregar la categoría faltante en lugar de desviar una.


2. Restricción estructurante: setValue solo transporta un escalar

Este es el punto que determina todo el diseño, y vale la pena plantearlo antes de las constantes.

Un comando parte de la interfaz, atraviesa el núcleo y llega a la integración mediante
device.setValue:

// server/lib/device/device.setValue.js
async function setValue(device, deviceFeature, value, options = {}) {
  const service = this.serviceManager.getService(device.service.name);
  await service.device.setValue(device, deviceFeature, value, options);
  // ...
}

value es un escalar — un número o una cadena. Sin embargo, el control PTZ solicitado
incluye ocho parámetros:

Parámetro Valores
Pan LEFT, RIGHT
Tilt UP, DOWN
Zoom ZOOM_IN, ZOOM_OUT
Distancia coeficiente de movimiento, de 0 a 1
Velocidad coeficiente de velocidad, de 0 a 1
Modo de movimiento ContinuousMove, RelativeMove, AbsoluteMove, GotoPreset, Stop
Duración continua para ContinuousMove, la duración en segundos antes de detenerse
Preset el token del preset al que unirse, con GotoPreset

Estos ocho parámetros no caben en un escalar. Tres formas de resolver:

Opción A — Una característica por comando, los ajustes en parámetros del dispositivo

Cada dirección se convierte en una característica de tipo push, y distance / speed /
continuous duration se convierten en parámetros del dispositivo (device.params),
ajustados una vez en la configuración.

camera/ptz-left     push    → se mueve a la izquierda, con los ajustes del dispositivo
camera/ptz-right    push
camera/ptz-up       push
camera/ptz-down     push
camera/ptz-zoom-in  push
camera/ptz-zoom-out push
camera/ptz-stop     push
camera/ptz-preset   string  → el token del preset al que unirse

A favor: no requiere cambios en el núcleo. PushDeviceFeature ya existe y renderiza
un botón; las escenas ya saben disparar una característica push. Inmediatamente
implementable.

En contra: la velocidad y la distancia ya no son ajustables por comando — una escena no
puede decir « gira suavemente ». Siete características para un solo dispositivo pesa la lista.

Opción B — Una característica única que lleva un comando serializado

Una sola característica camera/ptz, de tipo string, cuyo valor es un JSON:

{ "mode": "ContinuousMove", "pan": "LEFT", "speed": 0.5, "duration": 2 }

A favor: cubre los ocho parámetros sin cambiar nada en el núcleo — setValue ya acepta
cadenas (y no las persiste, lo cual es adecuado: un comando no es un estado).

En contra: opaco. La interfaz no puede construir un formulario a partir de una
cadena libre, y el editor de escenas mostraría un campo de texto donde el usuario debería
tipear JSON. Es una API para desarrolladores, no para el usuario final.

Opción C — Extender setValue con parámetros nombrados (recomendado)

setValue ya recibe un objeto options que transmite tal cual al servicio. Basta con
usarlo para los parámetros secundarios, la value llevando el comando principal.

// La integración recibe:
setValue(device, feature, 'LEFT', { speed: 0.5, distance: 0.3, duration: 2 });

A favor: cubre los ocho parámetros, mantiene un valor principal legible (por lo tanto
displayable y scriptable), y no introduce ninguna ruptura — options existe y ya está propagado.
La interfaz puede construir un formulario real, ya que cada parámetro está nombrado y tipado.

En contra: requiere definir qué options son válidos por tipo de característica, y que
el editor de escenas sepa presentarlos.

Recomendación: apuntar a C, entregando A como primer paso. A es
implementable inmediatamente y ya cubre « ve a la posición X cuando Y ocurre »,
que es el uso dominante; C luego agrega el ajuste fino sin invalidar A.


3. Constantes a agregar al SDK

A agregar en server/utils/constants.js de Gladys, luego reflejar en
lib/device-constants.js del SDK — este último, por convención documentada al inicio de
archivo, es un espejo estricto del primero.

3.1 Tipos de características

CAMERA: {
  IMAGE: 'image',
  // --- PTZ: control de una cámara motorizada ---
  // Direcciones. El nombre lleva el eje, no el sentido del movimiento esperado por el
  // protocolo: una cámara montada en el techo puede tener un eje invertido, lo que
  // se ajusta en la integración y no en la semántica de la característica.
  PTZ_LEFT: 'ptz-left',
  PTZ_RIGHT: 'ptz-right',
  PTZ_UP: 'ptz-up',
  PTZ_DOWN: 'ptz-down',
  PTZ_ZOOM_IN: 'ptz-zoom-in',
  PTZ_ZOOM_OUT: 'ptz-zoom-out',
  // Detención de un movimiento continuo. Esencial y no solo práctico:
  // un ContinuousMove sin Stop deja la cámara girando hasta su tope.
  PTZ_STOP: 'ptz-stop',
  // Posición registrada a alcanzar. El valor es el token del preset tal como
  // la cámara lo nombra, nunca un índice: las cámaras ONVIF devuelven
  // tokens opacos y no una lista ordenada.
  PTZ_PRESET: 'ptz-preset',
  // Posición absoluta, para las cámaras que saben reportarla. Separada de
  // las direcciones porque es legible Y inscribible, donde una dirección
  // es solo una orden.
  PTZ_POSITION_PAN: 'ptz-position-pan',
  PTZ_POSITION_TILT: 'ptz-position-tilt',
},

3.2 Precedente en el código existente

La adición sigue un patrón ya presente: TELEVISION lleva LEFT, RIGHT, UP, DOWN,
STOP como tipos de características distintos, exactamente para expresar una cruz
direccional.

TELEVISION: {
  // ...
  LEFT: 'left',
  RIGHT: 'right',
  UP: 'up',
  DOWN: 'down',
  // ...
},

La propuesta no crea un precedente: lo aplica a las cámaras, complementándolo con lo que el
PTZ exige además (presets, parada, posición absoluta).

3.3 Valores de los parámetros (opción C)

Si se elige la opción C, los coeficientes necesitan un dominio explícito:

const PTZ_MOVE_MODES = {
  CONTINUOUS: 'ContinuousMove',
  RELATIVE: 'RelativeMove',
  ABSOLUTE: 'AbsoluteMove',
  GOTO_PRESET: 'GotoPreset',
  STOP: 'Stop',
};

// `speed` y `distance` son coeficientes de 0 a 1, intencionalmente sin unidad:
// una cámara expresa su velocidad en grados por segundo, otra en pasos de motor, y
// ninguna lo documenta. El coeficiente es la única magnitud portable, y
// la integración la traduce a lo que su protocolo espera.
const PTZ_COEFFICIENT_MIN = 0;
const PTZ_COEFFICIENT_MAX = 1;

Nombrar los modos según la terminología ONVIF es deliberado: es el vocabulario del
estándar que la mayoría de las cámaras implementan, y traducirlo solo añadiría una
capa de correspondencia para mantener.


4. Widget de control de cámara

4.1 Extender el widget existente en lugar de crear uno segundo

Gladys ya tiene un widget de cámara (front/src/components/boxs/camera/Camera.jsx) que
display la imagen y, para las cámaras compatibles, el flujo en vivo.

Un widget PTZ separado obligaría al usuario a colocar dos cajas lado a lado para una
sola cámara, y mantenerlas alineadas. La propuesta es, por lo tanto, agregar los controles
al widget existente
, mostrados solo si el dispositivo tiene características PTZ.

4.2 Disposición propuesta

┌─────────────────────────────────┐
│                                 │
│         image / live            │
│                                 │
│                    ┌───┐        │   ← superposición, esquina inferior derecha
│                    │ ▲ │        │
│                ┌───┼───┼───┐    │
│                │ ◄ │ ■ │ ► │    │     ■ = stop
│                └───┼───┼───┘    │
│                    │ ▼ │        │
│                    └───┘        │
│  [ Entrada ▾ ]           [-] [+] │   ← presets            zoom
└─────────────────────────────────┘

Puntos de diseño, cada uno motivado:

  • Superposición, no debajo. El widget de la cámara a menudo se coloca en un formato pequeño; una
    fila de botones adicional debajo de la imagen consumiría la altura que se usa para ver la imagen.
  • Controles ocultos por defecto, revelados al pasar el ratón (y siempre visibles en táctil, donde
    no hay pasar el ratón). Un tablero consultado de un vistazo no necesita ocho botones permanentemente.
  • Presionar y mantener = movimiento continuo. mousedown inicia la dirección, mouseup
    inicia PTZ_STOP. Es el gesto que todo el mundo conoce de las interfaces de cámara,
    y corresponde exactamente a la pareja ContinuousMove / Stop.
    Un clic simple se reduce a un RelativeMove de un paso.
  • Presets en una lista desplegable, no en botones: su número varía de una cámara a otra y sus
    nombres son libres.
  • Zoom separado de la cruz direccional, porque no todas las cámaras motorizadas hacen zoom — los
    botones solo aparecen si las características correspondientes existen.

4.3 Renderizado de características fuera del widget

Independientemente del widget, las características PTZ aparecen en la vista « dispositivo en una
habitación ». El enrutamiento se realiza en front/src/components/boxs/device-in-room/DeviceRow.jsx:

const ROW_TYPE_BY_FEATURE_TYPE = {
  // ...
  [DEVICE_FEATURE_TYPES.CAMERA.PTZ_LEFT]: PushDeviceFeature,
  [DEVICE_FEATURE_TYPES.CAMERA.PTZ_RIGHT]: PushDeviceFeature,
  [DEVICE_FEATURE_TYPES.CAMERA.PTZ_UP]: PushDeviceFeature,
  [DEVICE_FEATURE_TYPES.CAMERA.PTZ_DOWN]: PushDeviceFeature,
  [DEVICE_FEATURE_TYPES.CAMERA.PTZ_ZOOM_IN]: PushDeviceFeature,
  [DEVICE_FEATURE_TYPES.CAMERA.PTZ_ZOOM_OUT]: PushDeviceFeature,
  [DEVICE_FEATURE_TYPES.CAMERA.PTZ_STOP]: PushDeviceFeature,
  [DEVICE_FEATURE_TYPES.CAMERA.PTZ_POSITION_PAN]: MultiLevelDeviceFeature,
  [DEVICE_FEATURE_TYPES.CAMERA.PTZ_POSITION_TILT]: MultiLevelDeviceFeature,
};

PushDeviceFeature y MultiLevelDeviceFeature ya existen: siete de los nueve tipos
por lo tanto no requieren ningún componente nuevo. Solo PTZ_PRESET requiere uno — una lista
desplegable alimentada por los presets de la cámara — y un componente PtzControl que agrupe
la cruz direccional sería deseable para evitar mostrar siete líneas de botones apilados.

Nota: una característica cuyo tipo no está en esta tabla no falla, se renderiza como un sensor. Las características PTZ serían
visibles pero no controlables hasta que el enrutamiento no se añada — lo que permite entregar el SDK y la interfaz
por separado.


5. Uso en escenas

Este es el principal interés en comparación con las acciones de integración, que no son
scriptables.

Con la opción A, una escena « alguien toca el timbre → la cámara mira la entrada » se escribe con
la acción existente « cambiar el estado de un dispositivo », ajustando camera/ptz-preset al token del preset. No se
necesita ninguna nueva acción de escena.

Con la opción C, el editor de escenas gana al proponer los parámetros nombrados (velocidad,
distancia, duración) en el formulario de esta acción, en lugar de dejarlos en los ajustes
del dispositivo.


6. Desglose sugerido

Cada paso tiene un valor propio y puede ser entregado solo:

  1. Constantes en el SDK y el núcleo — los tipos CAMERA.PTZ_*. Sin efecto visible,
    pero desbloquea inmediatamente las integraciones: una integración puede publicar las características
    y ser controlada por la API, antes de que la interfaz sepa cómo mostrarlas.
  2. Enrutamiento en DeviceRow.jsx — reutiliza PushDeviceFeature y
    MultiLevelDeviceFeature. Renderiza las características controlables desde la vista del dispositivo a un costo muy bajo.
  3. Componente PtzControl — la cruz direccional agrupada y la lista de presets.
  4. Integración al widget de la cámara — la superposición descrita en 4.2.
  5. Parámetros nombrados (opción C) — velocidad, distancia, duración por comando, y su
    presentación en el editor de escenas.

Los pasos 1 y 2 son suficientes para hacer que el PTZ sea utilizable de principio a fin, escenas incluidas.

¡Gracias por la respuesta, es muy pertinente! Lo he pasado a Fable para análisis y propuesta de especificación e implementación.

¡Te mantendré informado!

He iterado con Fable para algo más preciso, y para tener en cuenta las supported_options, la gran novedad reciente de Gladys :slight_smile:

La especificación :

Dime qué opinas

Funciona, lo miro esta noche.

Me parece bien y aún mejor que lo que se había propuesto.

Buenas noches @Will_71

Tengo 4 cámaras ONVIF (justamente compradas por esta razón). No dudes en contactarme si necesitas pruebas de esta integración.

Gracias por tu implicación y por los compartidos que haces disfrutar a los usuarios de Gladys :heart:

Jean

@Will_71 ¡La PR está lista para ser probada!

https://github.com/GladysAssistant/Gladys/pull/2762

Imagen Docker:

ghcr.io/gladysassistant/gladys-preview:claude-onvif-camera-spec-foh27o

Ok :+1:.
Voy a hacer una prueba este fin de semana. Te aviso

@Will_71 ¿Al final lo pudiste probar? :slight_smile:

Tenía que hacerlo aquí y me surgió un imprevisto, no he tocado mi PC ayer.
Y el primer día de vacaciones, empiezo haciendo mecánica en mi moto, así que me llevará un poco más de tiempo. Lo hago lo antes posible

@pierre-gilles He implementado la función en las cámaras TAPO.
Aquí tienes un primer informe:

Avanzamos, tengo más funciones en mi cámara, como el control de movimiento PTZ

Pero por ahora no funciona. Tengo que averiguar por qué, si es mi integración o algo más. El problema es que no tengo ningún registro cuando presiono los botones.


El control en la imagen de la cámara está bien, pero las acciones de abajo ya no son posibles.

¡Qué chulo, gracias por probarlo!

En cuanto a los botones de la imagen, efectivamente no parece muy controlable, ¿me confirmas eso?

¿No es muy práctico de usar?

¿Quieres que haga correcciones en la PR? :slight_smile:

Sí, entonces, los botones debajo de las flechas ya no son utilizables.
Deberían mostrarse solo cuando se está en directo, porque en un instantánea no tiene sentido tener la cruz para controlar la cámara.

@Will_71 ¡Está corregido! :blush: Dime si es mejor.