@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:
- 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.
- Enrutamiento en
DeviceRow.jsx — reutiliza PushDeviceFeature y
MultiLevelDeviceFeature. Renderiza las características controlables desde la vista del dispositivo a un costo muy bajo.
- Componente
PtzControl — la cruz direccional agrupada y la lista de presets.
- Integración al widget de la cámara — la superposición descrita en 4.2.
- 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.