Integración externa - Apple TV

Hola a todos,

Después de más de diez años siguiendo este proyecto (Gladys) sin realmente tomarme el tiempo de instalarlo y configurarlo, finalmente estoy de vuelta con una primera integración externa: Apple TV.

Se basa en pyatv en Python (como HA) y node-pyatv en Node.js (wrapper).

Aviso: La integración ha sido fuertemente desarrollada por GPT 5.6 Sol (Codex).

¡Bravo por la integración! Me interesa porque tengo una Apple TV en casa :grin: ¡Voy a probarla!

Lo he probado y me sale este error:

Quizá Codex sea menos riguroso que Claude aha, pero yo Claude siempre pienso en ejecutar el validador antes de publicar:

El comando:

npx github:GladysAssistant/integration-store .

¡Ay! Lo miro un poco más tarde… es extraño porque hice una instalación anoche y funcionaba :o

El error es:

{
    "status": 422,
    "code": "UNPROCESSABLE_ENTITY",
    "properties": "actions[1].depends_on: campo desconocido ; actions[2].depends_on: campo desconocido"
}

Voy a añadir el error en la interfaz para que sea más fácil de depurar :slight_smile:

Nueva versión disponible, vuelve a intentarlo y dime si hay que corregir algo :slight_smile:

Esta vez logré instalar, pero tengo un error en el escaneo:

Le pregunté a Claude y es un error de la integración, no de Gladys:

[2026-08-03T12:51:38.091Z] [INFO] [apple-tv] Iniciando la integración de Apple TV de Gladys
[2026-08-03T12:51:38.288Z] [INFO] [gladys-sdk] conectado a Gladys (http://172.30.0.1:80)
[2026-08-03T12:51:41.621Z] [INFO] [apple-tv] Iniciando descubrimiento mediado de Apple TV
[2026-08-03T12:51:42.339Z] [INFO] [apple-tv] Iniciando descubrimiento mediado de Apple TV
[2026-08-03T12:51:42.356Z] [ERROR] [apple-tv] Falló el descubrimiento mediado de mDNS GladysApiError: Conflicto
    at HttpClient.request (/app/node_modules/@gladysassistant/integration-sdk/lib/http-client.js:49:13)
    at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
    at async AppleTvIntegration.scan (file:///app/src/integration.js:76:29)
    at async GladysIntegration._runHandler (/app/node_modules/@gladysassistant/integration-sdk/lib/gladys-integration.js:1166:7)
    at async GladysIntegration._handleMessage (/app/node_modules/@gladysassistant/integration-sdk/lib/gladys-integration.js:1022:9) {
  status: 409,
  code: 'CONFLICT'
}
[2026-08-03T12:51:50.977Z] [ERROR] [apple-tv] Falló la prueba de Apple TV para 3 candidatos de host(s) Error: No se encontraron dispositivos, pero se recibió 1 error: [
  {
    "result": "failure",
    "datetime": "2026-08-03T14:51:50.763359+02:00",
    "exception": "Se esperaban 4 octetos en 'fd33:5f93:3f40:49cd:3:fd3b:c82a:7ced'",
    "stacktrace": "Traceback (última llamada):\n  File \"/opt/pyatv/lib/python3.11/site-packages/pyatv/scripts/atvscript.py\", línea 413, en appstart\n    args.output(await _handle_command(args, abort_sem, storage, loop)),\n                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/pyatv/lib/python3.11/site-packages/pyatv/scripts/atvscript.py\", línea 285, en _handle_command\n    return await _scan_devices(loop, storage, args.scan_hosts)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/pyatv/lib/python3.11/site-packages/pyatv/scripts/atvscript.py\", línea 231, en _scan_devices\n    for atv in await scan(loop, hosts=hosts, storage=storage):\n               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/pyatv/lib/python3.11/site-packages/pyatv/__init__.py\", línea 68, en scan\n    scanner = UnicastMdnsScanner([IPv4Address(host) for host in hosts], loop)\n                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/pyatv/lib/python3.11/site-packages/pyatv/__init__.py\", línea 68, en <listcomp>\n    scanner = UnicastMdnsScanner([IPv4Address(host) for host in hosts], loop)\n                                  ^^^^^^^^^^^^^^^^^\n  File \"/usr/lib/python3.11/ipaddress.py\", línea 1319, en __init__\n    self._ip = self._ip_int_from_string(addr_str)\n               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/usr/lib/python3.11/ipaddress.py\", línea 1206, en _ip_int_from_string\n    raise AddressValueError(\"Se esperaban 4 octetos en %r\" % ip_str)\nipaddress.AddressValueError: Se esperaban 4 octetos en 'fd33:5f93:3f40:49cd:3:fd3b:c82a:7ced'\n"
  }
]
    at e.find (file:///app/node_modules/@sebbo2002/node-pyatv/dist/lib-DynU3Xla.mjs:7:8365)
    at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
    at async AppleTvIntegration.scan (file:///app/src/integration.js:95:15)
    at async GladysIntegration._runHandler (/app/node_modules/@gladysassistant/integration-sdk/lib/gladys-integration.js:1166:7)
    at async GladysIntegration._handleMessage (/app/node_modules/@gladysassistant/integration-sdk/lib/gladys-integration.js:1022:9)
[2026-08-03T12:51:58.718Z] [INFO] [apple-tv] Iniciando descubrimiento mediado de Apple TV
[2026-08-03T12:52:07.997Z] [ERROR] [apple-tv] Falló la prueba de Apple TV para 3 candidatos de host(s) Error: No se encontraron dispositivos, pero se recibió 1 error: [
  {
    "result": "failure",
    "datetime": "2026-08-03T14:52:07.795239+02:00",
    "exception": "Se esperaban 4 octetos en 'fd33:5f93:3f40:49cd:3:fd3b:c82a:7ced'",
    "stacktrace": "Traceback (última llamada):\n  File \"/opt/pyatv/lib/python3.11/site-packages/pyatv/scripts/atvscript.py\", línea 413, en appstart\n    args.output(await _handle_command(args, abort_sem, storage, loop)),\n                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/pyatv/lib/python3.11/site-packages/pyatv/scripts/atvscript.py\", línea 285, en _handle_command\n    return await _scan_devices(loop, storage, args.scan_hosts)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/pyatv/lib/python3.11/site-packages/pyatv/scripts/atvscript.py\", línea 231, en _scan_devices\n    for atv in await scan(loop, hosts=hosts, storage=storage):\n               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/pyatv/lib/python3.11/site-packages/pyatv/__init__.py\", línea 68, en scan\n    scanner = UnicastMdnsScanner([IPv4Address(host) for host in hosts], loop)\n                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/pyatv/lib/python3.11/site-packages/pyatv/__init__.py\", línea 68, en <listcomp>\n    scanner = UnicastMdnsScanner([IPv4Address(host) for host in hosts], loop)\n                                  ^^^^^^^^^^^^^^^^^\n  File \"/usr/lib/python3.11/ipaddress.py\", línea 1319, en __init__\n    self._ip = self._ip_int_from_string(addr_str)\n               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/usr/lib/python3.11/ipaddress.py\", línea 1206, en _ip_int_from_string\n    raise AddressValueError(\"Se esperaban 4 octetos en %r\" % ip_str)\nipaddress.AddressValueError: Se esperaban 4 octetos en 'fd33:5f93:3f40:49cd:3:fd3b:c82a:7ced'\n"
  }
]
    at e.find (file:///app/node_modules/@sebbo2002/node-pyatv/dist/lib-DynU3Xla.mjs:7:8365)
    at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
    at async AppleTvIntegration.scan (file:///app/src/integration.js:95:15)
    at async GladysIntegration._runHandler (/app/node_modules/@gladysassistant/integration-sdk/lib/gladys-integration.js:1166:7)
    at async GladysIntegration._handleMessage (/app/node_modules/@gladysassistant/integration-sdk/lib/gladys-integration.js:1022:9)

¡Vaya! :joy: ¡Acabo de hacer un nuevo lanzamiento! :+1: (en progreso…)
Si aún no funciona, mientras tanto, puedes poner la IP de tu Apple TV directamente en la pestaña de configuración.

Avanzamos, pero esta vez tengo otro error:

El selector podría ser un poco más único que eso ^^

Todavía tiene que mejorar, Codex :joy:

Quizás fui un poco « tacaño » en el razonamiento utilizado jaja
Pero es cierto que no se molestó en absoluto en este caso :joy:

¡Nueva versión disponible! Le pedí que se auto-revise y que corrija otros posibles problemas de este estilo.

No dudes en decírmelo si encuentras otros problemas si es necesario

Esta vez he podido añadir el Apple TV :slight_smile:

Por otro lado, el nombre del dispositivo podría contener « Apple TV », porque ahora solo es el nombre de la habitación (debe ser el nombre que Apple le dio a mi Apple TV), y por lo tanto no es muy legible:

Por cierto, intenté encender el Apple TV con el tipo « Power », pero no hace nada:

Los registros no muestran nada especial:

[2026-08-03T15:22:33.784Z] [INFO] [apple-tv] Starting the Gladys Apple TV integration
[2026-08-03T15:22:33.982Z] [INFO] [gladys-sdk] connected to Gladys (http://172.30.0.1:80)
[2026-08-03T15:22:39.142Z] [INFO] [apple-tv] Starting mediated Apple TV discovery
[2026-08-03T15:22:48.502Z] [INFO] [apple-tv] Discovery completed: 1 Apple TV device(s) from 1 candidate host(s)
[2026-08-03T15:24:50.959Z] [WARN] [apple-tv] Apple TV real-time updates unavailable for Salon: Got pyatv Error: power_state is not supported; using polling
[2026-08-03T15:24:55.556Z] [WARN] [apple-tv] Apple TV real-time updates unavailable for Salon: Got pyatv Error: power_state is not supported; using polling

¿Funciona en tu caso?

Atención, tu integración ha sido listada en la tienda de integraciones aunque aún no es funcional. La idea es publicarla solo cuando esté operativa. :wink:

Mientras todo funcione, puedes retirar el tema de GitHub. Así evitarás que los usuarios la descubran demasiado pronto y se decepcionen durante sus pruebas. :slightly_smiling_face:

Ups… ¡lo he retirado! Codex ha puesto la etiqueta sin avisarme. ¡Lo siento!

Miro para corregir, pero sí, Power funcionaba en mi casa anoche.

¡Hola a todos!

Bueno… lo he « reescrito » todo desde cero con Claude Opus 5, no es un parche sobre la versión antigua, es una nueva implementación.

Lo que cambia en el enfoque

La versión anterior pasaba por node-pyatv, un wrapper de Node que llama a la CLI atvremote como subproceso. Funciona para enviar una tecla, pero se pierde todo lo demás: cada comando reinicia un proceso, cada sesión de Apple TV tarda uno o dos segundos en abrirse, y las actualizaciones en tiempo real no existen, solo viven mientras se mantiene la conexión.

La nueva versión incluye un puente de Python que habla directamente con pyatv. El proceso de Node mantiene el lado de Gladys (SDK, dispositivos, acciones) y controla un worker de Python persistente a través de un tubo, en JSON lines. En concreto:

  • una sesión abierta permanentemente por Apple TV → los botones responden de inmediato, y el estado (reproducción/pausa, lo que está en pantalla, la aplicación en curso, el volumen) se envía en push, no en polling;
  • toda la API de pyatv está accesible, no solo lo que la CLI expone: emparejamiento de los dos protocolos, detección de las capacidades reales del dispositivo, listeners, lista de aplicaciones instaladas;
  • el emparejamiento se realiza desde la pantalla de configuración. tvOS 15+ requiere dos juegos de credenciales — AirPlay y luego Companion — por lo que dos códigos se muestran en la televisión. La integración encadena los dos pasos automáticamente, y si un código expira (la conexión que lo lleva se cierra después de un minuto) muestra un nuevo código en lugar de dejarte atascado.

Qué ofrece en Gladys

  • El control remoto clickeable en un tablero (PR #2765): cruceta direccional, OK, Atrás, Inicio, Centro de control, reproducción/pausa, teclas de transporte, cursor de volumen. Añade una caja « Dispositivos » y elige tus teclas.
  • La caja Música para controlar la reproducción como un Sonos.
  • Sensores de texto « Reproducción en curso » y « Aplicación ».
  • Un atajo por aplicación instalada, para que una escena abra Netflix o Disney+ directamente.
  • Alimentación, standby y descubrimiento automático a través de los anuncios AirPlay de la red.

Las funcionalidades condicionales (volumen, alimentación, aplicaciones) solo se publican una vez que una conexión real ha demostrado que el dispositivo las gestiona, no hay un cursor de volumen que nunca se mueva.

Probado en una Apple TV 4K (3ᵉ generación) con tvOS 26: emparejamiento, control remoto, aplicaciones, envío de estado en tiempo real.

Puedes probarlo

La imagen de desarrollo es pública:

ghcr.io/valentinhttr/gladys-apple-tv:dev

Un punto a conocer: si ejecutas Gladys en Docker en un Mac o en Windows, el descubrimiento automático no puede funcionar, ya que, por ejemplo, Docker Desktop, OrbStack, etc. ejecutan los contenedores en una VM Linux, por lo que network_mode: host designa la red de la VM y el multicast de la Apple TV nunca llega. El unicast funciona muy bien: introduce la dirección IP de tu Apple TV en « Direcciones IPv4 manuales » y todo lo demás funciona normalmente. En un Gladys instalado en Linux (Raspberry Pi, NAS, servidor), no hay nada que hacer.

El nuevo código: GitHub - valentinhttr/gladys-apple-tv: External Gladys Assistant integration for discovering, pairing and controlling Apple TV devices · GitHub

Aún no se ha publicado en la tienda, prefiero recoger comentarios primero. ¡No dudes en hacerlo!

¡Gracias por esta nueva versión, es mucho mejor, y esta vez funciona a la primera. :slightly_smiling_face:

Tengo algunos comentarios sobre la experiencia de usuario. Al principio, no entendí que tenía que iniciar el proceso de emparejamiento, ni que tenía que obtener la dirección IP de mi Apple TV antes de poder hacerlo.

En la pestaña Configuración, es posible añadir un selector de dispositivo antes de ejecutar una acción, y creo que es exactamente el caso de uso adecuado aquí. Así guiaría mucho mejor al usuario.

El texto también podría retocarse un poco para explicar con más claridad el proceso general de configuración.

Por cierto, un pequeño bug divertido: si uso los botones Izquierda o Derecha en la pantalla de inicio de la Apple TV, salta dos aplicaciones cada vez que me muevo en lugar de una. :grinning_face_with_smiling_eyes:

También me hace pensar que realmente debería desarrollarse un widget adecuado para este tipo de dispositivo. La lista de decenas de funciones no es muy agradable de usar. :sweat_smile: Pero mientras tanto, ya es un gran avance, y sobre todo ¡por fin permite crear escenas!

En cualquier caso, ¡muchas gracias por el desarrollo, es un gran añadido!

Gracias por la devolución detallada, todo está corregido :wink: No dudes en volver a probar y darme tus últimos comentarios antes de poner esto disponible en la tienda.

El selector de dispositivo: lo intenté al principio, pero el núcleo validaba los campos de acción contra field.options, que un select con source: "devices" no tiene, por lo que « debe ser uno de » y una lista vacía, sin importar lo que se envíe. Dejé un comentario en el código para explicar por qué me decidí por un campo de texto. Funciona desde la 4.85.0, por lo que las siete acciones ahora tienen una lista desplegable y nadie busca su dirección IP. La integración requiere Gladys >= 4.85.0.

Los textos: reescritos alrededor del desarrollo completo, con los nombres reales de los botones de la interfaz: una sección de introducción que despliega Escanear → Añadir a Gladys → emparejar → Actualizar, luego una sección por bloque de ajustes, y una última justo encima de los botones de acción. El mensaje de finalización del emparejamiento apunta explícitamente a « Actualizar » en lugar de decir vagamente que se acepte la actualización.

El salto de dos aplicaciones: bien visto, y el culpable no está donde yo esperaba. Verifiqué: nadie envía la tecla dos veces, ni Gladys, ni el front, ni mi bridge, ni pyatv. De hecho, tvOS no lee una tecla direccional como un evento, sino como un gesto, mide el intervalo entre el key-down y el key-up HID y lo pasa a un reconocedor de tap o de long-press (pyatv#792). En MRP, los dos pasan por el túnel AirPlay, y esta latencia es suficiente para que la pantalla de inicio crea que es una tecla mantenida y se auto-repite.

Las teclas de navegación ahora van a Companion, el protocolo que habla la corona presionable de un verdadero Siri Remote, en lugar de MRP que la fachada pyatv elige por defecto. Los comandos multimedia se quedan en MRP, que tiene verdaderas semánticas de reproducción.

No he podido confirmar en un televisor antes de publicar, por lo que si puedes volver a decirme si el enfoque avanza bien de una aplicación ahora, me ayudaría.

El widget: completamente de acuerdo, la lista de funciones no es un control remoto. Ahí está del lado del núcleo y no puedo hacer nada desde la integración. Mientras tanto, podemos desactivar los accesos directos de aplicaciones en la configuración, lo que ya elimina hasta 25 líneas.

Todo esto está en la imagen de desarrollo: ghcr.io/valentinhttr/gladys-apple-tv:dev

La integración estará disponible en ~1h en la tienda, ya que me parece bastante estable, especialmente para crear escenas.

Está destinada a evolucionar, no dudes en darme tus comentarios aquí directamente :slight_smile:

Hola,

Gracias por la integración
Por mi parte, estoy en una red separada entre Gladys y mi Apple TV
Por lo tanto, he proporcionado la IP de la Apple TV y he abierto todos los puertos que veía bloqueados entre mis dos redes
Airplay está bien activado y lo he probado poniendo en la Apple TV « Acceso para todos »
Mi instalación de Gladys tiene « network mode: host »
Pero la búsqueda no encuentra nada
También he notado que la integración intentó escanear algunas IP alrededor de la IP de mi Apple TV

Aquí están los registros:

[2026-08-16T07:52:30.184Z] [INFO] [apple-tv] Gladys capturó 5 anuncios de AirPlay
[2026-08-16T07:52:30.184Z] [INFO] [apple-tv] Verificando 5 direcciones candidatas con pyatv
[2026-08-16T07:52:38.186Z] [INFO] [apple-tv] Se encontraron 0 Apple TV(s)
[2026-08-16T08:07:18.324Z] [INFO] [apple-tv] Escaneo solicitado

¿Alguna idea? :slight_smile:

Hola,

¡Gracias por tu respuesta!
Según GPT 5.6 Sol xHigh, la causa es la siguiente:

La causa es una limitación real de la integración 1.0.0, no una mala configuración del usuario.
La dirección manual se añade a las candidatas en discovery.js, luego se transmite a pyatv.scan(hosts=…) en pyatv_bridge.py. Sin embargo, esta operación sigue siendo una solicitud mDNS directa en UDP 5353. pyatv especifica explícitamente que no funciona entre subredes, ya que el Apple TV ignora las solicitudes provenientes de otra red. Esto es conforme a la documentación de pyatv y a la sección 5.5 de RFC 6762.

El network_mode: host no cambia la separación entre VLANs. « Acceso a todos » se refiere a la autorización de AirPlay, no a la respuesta mDNS. Abrir UDP 5353 tampoco es suficiente cuando la solicitud conserva una dirección de origen ubicada en la otra subred.

La corrección requiere una evolución por parte de Gladys, ya que el contenedor de la integración está en una red Docker bridge y no recibe el multicast mDNS. Solo el núcleo de Gladys, ubicado en la red del host, puede capturar estos anuncios.

Actualmente, el descubrimiento mediado solo transmite un servicio mDNS declarado en el manifiesto. Sin embargo, pyatv necesita al menos los anuncios _airplay._tcp y _companion-link._tcp para recuperar los identificadores, propiedades TXT y puertos dinámicos. De lo contrario, la integración intenta solicitarlos directamente al Apple TV, pero este ignora la solicitud cuando proviene de otra subred.

@Pierre-Gilles, ¿sería posible que Gladys permita declarar y recuperar varios servicios mDNS para una misma integración?

De lo contrario, ¿tienen alguna otra idea?