Intégration externe - Apple TV

Bonjour à tous,

Après plus de dix ans à suivre ce projet (Gladys) sans vraiment prendre le temps de l’installer et de le configurer, me voilà enfin de retour avec une première intégration externe : Apple TV.

Elle s’appuie sur pyatv en Python (comme HA) et node-pyatv côté Node.js (wrapper).

Disclaimer : L’intégration a été fortement développée par GPT 5.6 Sol (Codex)

Bravo pour l’intégration, ça m’intéresse parce que j’ai une Apple TV chez moi :grin: je vais tester !

J’ai testé et j’ai cette erreur :

Codex est peut-être moins rigoureux que Claude aha, moi Claude pense toujours à faire tourner le validateur avant de publier :

La commande :

npx github:GladysAssistant/integration-store .

Aie ! Je regarde ça un peu plus tard… c’est étrange car j’ai fait une installation hier soir et ça fonctionnait :o

L’erreur c’est:

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

Je vais rajouter l’erreur dans l’interface comme ça c’est plus facile à débug :slight_smile:

Nouvelle release dispo, réessaye et dis-moi s’il faut corriger quelque chose :slight_smile:

Cette fois-ci j’arrive à installer mais j’ai une erreur au scan :

J’ai demandé à Claude et c’est bien un bug de l’intégration, pas de Gladys :

[2026-08-03T12:51:38.091Z] [INFO] [apple-tv] Starting the Gladys Apple TV integration
[2026-08-03T12:51:38.288Z] [INFO] [gladys-sdk] connected to Gladys (http://172.30.0.1:80)
[2026-08-03T12:51:41.621Z] [INFO] [apple-tv] Starting mediated Apple TV discovery
[2026-08-03T12:51:42.339Z] [INFO] [apple-tv] Starting mediated Apple TV discovery
[2026-08-03T12:51:42.356Z] [ERROR] [apple-tv] Mediated mDNS discovery failed GladysApiError: Conflict
    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] Apple TV probe failed for 3 candidate host(s) Error: Unable to find any devices, but received 1 error: [
  {
    "result": "failure",
    "datetime": "2026-08-03T14:51:50.763359+02:00",
    "exception": "Expected 4 octets in 'fd33:5f93:3f40:49cd:3:fd3b:c82a:7ced'",
    "stacktrace": "Traceback (most recent call last):\n  File \"/opt/pyatv/lib/python3.11/site-packages/pyatv/scripts/atvscript.py\", line 413, in 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\", line 285, in _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\", line 231, in _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\", line 68, in scan\n    scanner = UnicastMdnsScanner([IPv4Address(host) for host in hosts], loop)\n                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/pyatv/lib/python3.11/site-packages/pyatv/__init__.py\", line 68, in <listcomp>\n    scanner = UnicastMdnsScanner([IPv4Address(host) for host in hosts], loop)\n                                  ^^^^^^^^^^^^^^^^^\n  File \"/usr/lib/python3.11/ipaddress.py\", line 1319, in __init__\n    self._ip = self._ip_int_from_string(addr_str)\n               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/usr/lib/python3.11/ipaddress.py\", line 1206, in _ip_int_from_string\n    raise AddressValueError(\"Expected 4 octets in %r\" % ip_str)\nipaddress.AddressValueError: Expected 4 octets in '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] Starting mediated Apple TV discovery
[2026-08-03T12:52:07.997Z] [ERROR] [apple-tv] Apple TV probe failed for 3 candidate host(s) Error: Unable to find any devices, but received 1 error: [
  {
    "result": "failure",
    "datetime": "2026-08-03T14:52:07.795239+02:00",
    "exception": "Expected 4 octets in 'fd33:5f93:3f40:49cd:3:fd3b:c82a:7ced'",
    "stacktrace": "Traceback (most recent call last):\n  File \"/opt/pyatv/lib/python3.11/site-packages/pyatv/scripts/atvscript.py\", line 413, in 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\", line 285, in _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\", line 231, in _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\", line 68, in scan\n    scanner = UnicastMdnsScanner([IPv4Address(host) for host in hosts], loop)\n                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/pyatv/lib/python3.11/site-packages/pyatv/__init__.py\", line 68, in <listcomp>\n    scanner = UnicastMdnsScanner([IPv4Address(host) for host in hosts], loop)\n                                  ^^^^^^^^^^^^^^^^^\n  File \"/usr/lib/python3.11/ipaddress.py\", line 1319, in __init__\n    self._ip = self._ip_int_from_string(addr_str)\n               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/usr/lib/python3.11/ipaddress.py\", line 1206, in _ip_int_from_string\n    raise AddressValueError(\"Expected 4 octets in %r\" % ip_str)\nipaddress.AddressValueError: Expected 4 octets in '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)

Décidément :joy: ! Je viens de faire une nouvelle release :+1: (en cours…)
Si ça ne fonctionne toujours pas, en attendant, tu peux déjà mettre l’IP de ton apple TV directement dans l’onglet config.

On avance, mais cette fois-ci j’ai une autre erreur :

Le selector pourrait-être un peu plus unique que ça ^^

Il a des progrès à faire Codex :joy:

J’ai peut être été un peu « radin » sur le raisonnement utilisé haha
Mais c’est vrai qu’il s’est pas embêté pour le coup :joy:

Nouvelle release disponible, je lui ai demandé de s’auto-review et de corriger d’autres potentiels problèmes du style !

Hésite pas à me dire si tu rencontres d’autres soucis si besoin

J’ai pu ajouter l’Apple TV cette fois-ci :slight_smile:

Par contre, le nom de l’appareil pourrait contenir « Apple TV », parce que là c’est juste le nom de la pièce (ça doit être le nom donné par Apple à mon Apple TV), et donc c’est pas très lisible :

Sinon, j’ai essayé d’allumer l’Apple TV avec le type « Power », mais ça ne fait rien :

Les logs rien de spécial :

[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

Ça marche chez toi ?

Attention, ton intégration a été listée sur le store des intégrations alors qu’elle n’est pas encore fonctionnelle. L’idée est de ne la publier qu’une fois qu’elle est opérationnelle. :wink:

En attendant que tout fonctionne, tu peux retirer le topic GitHub. Ça évitera que des utilisateurs la découvrent trop tôt et soient déçus lors de leurs tests. :slightly_smiling_face:

Oups… j’ai retiré ! Codex a mis le tag sans me prévenir ! Désolé

Je regarde pour corriger, mais oui Power fonctionnait chez moi hier soir.

Salut à tous !

Bon… J’ai tout « réécrit » from scratch avec Claude Opus 5, ce n’est pas un correctif sur l’ancienne version, c’est une nouvelle implémentation.

Ce qui change dans l’approche

L’ancienne version passait par node-pyatv, un wrapper Node qui appelle la CLI atvremote en sous-processus. Ça marche pour envoyer une touche, mais on perd tout le reste : chaque commande relance un processus, chaque session Apple TV coûte une à deux secondes à ouvrir, et les mises à jour temps réel n’existent pas, elles ne vivent que tant que la connexion est tenue.

La nouvelle version embarque un bridge Python qui parle directement à pyatv. Le process Node garde le côté Gladys (SDK, appareils, actions) et pilote un worker Python persistant par un tube, en JSON lines. Concrètement :

  • une session ouverte en permanence par Apple TV → les boutons répondent tout de suite, et l’état (lecture/pause, ce qui est à l’écran, l’app en cours, le volume) remonte en push, pas en polling ;
  • toute l’API de pyatv est accessible, pas seulement ce que la CLI expose : appairage des deux protocoles, détection des capacités réelles de l’appareil, listeners, liste des applications installées ;
  • l’appairage se fait depuis l’écran de configuration. tvOS 15+ demande deux jeux d’identifiants — AirPlay puis Companion — donc deux codes affichés sur la télé. L’intégration enchaîne les deux étapes toute seule, et si un code expire (la connexion qui le porte se ferme au bout d’une minute) elle en réaffiche un nouveau au lieu de vous laisser dans le mur.

Ce que ça donne dans Gladys

  • La télécommande cliquable sur un tableau de bord (PR #2765) : croix directionnelle, OK, Retour, Accueil, Centre de contrôle, lecture/pause, touches de transport, curseur de volume. Ajoutez une boîte « Appareils » et choisissez vos touches.
  • La boîte Musique pour piloter la lecture comme un Sonos.
  • Capteurs texte « Lecture en cours » et « Application ».
  • Un raccourci par application installée, pour qu’une scène ouvre Netflix ou Disney+ directement.
  • Alimentation, veille, et découverte automatique via les annonces AirPlay du réseau.

Les fonctionnalités conditionnelles (volume, alimentation, applications) ne sont publiées qu’une fois qu’une connexion réelle a prouvé que l’appareil les gère, pas de curseur de volume qui ne bougera jamais.

Testé sur une Apple TV 4K (3ᵉ génération) sous tvOS 26 : appairage, télécommande, applications, remontée d’état temps réel.

Vous pouvez la tester

L’image de développement est publique :

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

Un point à connaître : si vous faites tourner Gladys dans Docker sur un Mac ou sous Windows, la découverte automatique ne peut pas marcher, car par exemple Docker Desktop, OrbStack, etc. exécutent les conteneurs dans une VM Linux, donc network_mode: host désigne le réseau de la VM et le multicast de l’Apple TV n’arrive jamais. L’unicast passe très bien : renseignez l’adresse IP de votre Apple TV dans « Adresses IPv4 manuelles » et tout le reste fonctionne normalement. Sur un Gladys installé sur Linux (Raspberry Pi, NAS, serveur), rien à faire.

Le nouveau code : GitHub - valentinhttr/gladys-apple-tv: External Gladys Assistant integration for discovering, pairing and controlling Apple TV devices · GitHub

Ce n’est pas encore publié dans le store, je préfère récolter des retours d’abord. N’hésitez pas !

Merci pour cette nouvelle version, c’est beaucoup mieux, et cette fois-ci ça fonctionne du premier coup. :slightly_smiling_face:

J’ai quand même quelques petits retours sur l’UX. Au début, je n’avais pas compris qu’il fallait lancer le processus d’appairage, ni que je devais récupérer l’adresse IP de mon Apple TV avant de pouvoir le faire.

Dans l’onglet Configuration, il est possible d’ajouter un sélecteur d’appareil avant d’exécuter une action, et je pense que c’est exactement le bon cas d’usage ici. Ça guiderait beaucoup mieux l’utilisateur.

Le texte pourrait aussi être un peu retravaillé pour expliquer plus clairement le déroulement global de la configuration.

Sinon, petit bug amusant : si j’utilise les boutons Gauche ou Droite sur l’écran d’accueil de l’Apple TV, ça saute deux applications à chaque déplacement au lieu d’une. :grinning_face_with_smiling_eyes:

Ça me fait aussi penser qu’il faudrait vraiment développer un vrai widget pour ce type d’appareil. La liste de dizaines de fonctionnalités n’est pas très agréable à utiliser. :sweat_smile: Mais en attendant, c’est déjà une énorme avancée, et surtout ça permet enfin de créer des scènes !

En tout cas, merci beaucoup pour le développement, c’est un super ajout !

Merci pour le retour détaillé, tout est corrigé :wink: N’hésite pas à re-tester et me faire tes derniers commentaires avant de rendre cela dispo sur le store !

Le sélecteur d’appareil : j’avais essayé au départ, mais le core validait les champs d’action contre field.options, qu’un select avec source: "devices" n’a pas, donc « must be one of » et une liste vide, quoi qu’on envoie. J’avais laissé un commentaire dans le code pour expliquer pourquoi je m’étais rabattu sur un input texte. Ça marche depuis la 4.85.0, donc les sept actions prennent maintenant une liste déroulante et plus personne ne cherche son adresse IP. L’intégration demande du coup Gladys >= 4.85.0.

Les textes : réécrits autour du déroulé complet, avec les vrais noms de boutons de l’UI : une section d’intro qui déroule Scanner → Ajouter à Gladys → appairer → Mettre à jour, puis une section par bloc de réglages, et une dernière juste au-dessus des boutons d’action. Le message de fin d’appairage pointe explicitement vers « Mettre à jour » au lieu de dire vaguement d’accepter la mise à jour.

Le saut de deux applications : bien vu, et le coupable n’est pas là où je l’attendais. J’ai vérifié : personne n’envoie la touche deux fois, ni Gladys, ni le front, ni mon bridge, ni pyatv. En fait tvOS ne lit pas une touche directionnelle comme un événement mais comme un geste, il mesure l’intervalle entre le key-down et le key-up HID et le passe à un reconnaisseur de tap ou de long-press (pyatv#792). En MRP les deux traversent le tunnel AirPlay, et cette latence suffit pour que l’écran d’accueil croie à une touche maintenue et auto-répète.

Les touches de navigation partent donc maintenant sur Companion, le protocole que parle la couronne cliquable d’une vraie Siri Remote, au lieu de MRP que la façade pyatv choisit par défaut. Les commandes média restent sur MRP, qui a de vraies sémantiques de lecture.

Je n’ai pas pu confirmer sur un téléviseur avant de publier, donc si tu peux redire si le focus avance bien d’une app maintenant, ça m’aiderait.

Le widget : complètement d’accord, la liste de fonctionnalités n’est pas une télécommande. Là c’est côté core et je ne peux rien faire depuis l’intégration. En attendant, on peut désactiver les raccourcis d’applications dans la config, ça enlève déjà jusqu’à 25 lignes.

Tout ça est sur l’image de dev : ghcr.io/valentinhttr/gladys-apple-tv:dev

L’intégration sera disponible dans ~1h dans le store car elle me semble assez stable, notamment pour créer des scènes !

Elle est vouée à évoluer, n’hésitez pas à me donner vos retours ici directement :slight_smile:

Hello,

Merci pour l’intégration
Je suis de mon côté sur un réseau separé entre Gladys et mon Apple TV
J’ai donc renseigné l’IP de l’Apple TV et ouvert tout les ports que je voyais bloqué entre mes 2 réseaux
Airplay est bien activé et j’ai testé en mettant sur l’apple TV « Accès tout le monde »
Mon installation gladys a bien « network mode: host »
Mais la recherche ne trouve rien
J’ai remarqué également que l’intégration tentait de scanner quelques IP autour de l’IP de mon Apple TV

Voici les logs :

[2026-08-16T07:52:30.184Z] [INFO] [apple-tv] Gladys captured 5 AirPlay announcement(s)
[2026-08-16T07:52:30.184Z] [INFO] [apple-tv] Verifying 5 candidate address(es) with pyatv
[2026-08-16T07:52:38.186Z] [INFO] [apple-tv] Found 0 Apple TV(s)
[2026-08-16T08:07:18.324Z] [INFO] [apple-tv] Scan requested

Une idée ? :slight_smile:

Hello,

Je te remercie pour ton retour !
Selon GPT 5.6 Sol xHigh, voici la cause :

La cause est une limitation réelle de l’intégration 1.0.0, pas une mauvaise configuration de l’utilisateur.
L’adresse manuelle est ajoutée aux candidates dans discovery.js, puis transmise à pyatv.scan(hosts=…) dans pyatv_bridge.py. Or cette opération reste une requête mDNS directe sur UDP 5353. pyatv précise explicitement qu’elle ne fonctionne pas entre sous-réseaux, car l’Apple TV ignore les requêtes provenant d’un autre lien réseau. C’est conforme à la documentation pyatv et à la section 5.5 de RFC 6762.

Le network_mode: host ne change pas la séparation entre VLANs. « Accès à tout le monde » concerne l’autorisation AirPlay, pas la réponse mDNS. Ouvrir UDP 5353 ne suffit pas non plus lorsque la requête conserve une adresse source située dans l’autre sous-réseau.

Le correctif nécessite une évolution côté Gladys, car le conteneur de l’intégration est sur un réseau Docker bridge et ne reçoit pas le multicast mDNS. Seul le core Gladys, placé sur le réseau de l’hôte, peut donc capturer ces annonces.

Actuellement, la découverte médiée ne transmet qu’un seul service mDNS déclaré dans le manifeste. Or pyatv a besoin au minimum des annonces _airplay._tcp et _companion-link._tcp pour récupérer les identifiants, propriétés TXT et ports dynamiques. L’intégration tente sinon de les redemander directement à l’Apple TV, mais celle-ci ignore cette requête lorsqu’elle vient d’un autre sous-réseau.

@Pierre-Gilles, serait-il envisageable que Gladys permette de déclarer et récupérer plusieurs services mDNS pour une même intégration ?

Sinon, avez-vous une autre idée ?