External integration - Apple TV

Hello everyone,

After more than ten years of following this project (Gladys) without really taking the time to install and configure it, I’m finally back with a first external integration: Apple TV.

It relies on pyatv in Python (like HA) and node-pyatv on the Node.js side (wrapper).

Disclaimer: The integration was heavily developed by GPT 5.6 Sol (Codex).

Great job on the integration, it interests me because I have an Apple TV at home :grin: I’m going to try it out!

I tested and got this error:

Codex might be less strict than Claude aha, but Claude always thinks to run the validator before publishing:

The command:

npx github:GladysAssistant/integration-store .

Oh! I’ll look at that a bit later… it’s strange because I did an installation last night and it was working :o

The error is:

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

I’ll add the error to the interface so it’s easier to debug :slight_smile:

New release available, try again and let me know if something needs fixing :slight_smile:

This time I managed to install but I have an error during the scan:

I asked Claude and it is indeed a bug in the integration, not 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)

Well, well! :joy: I just made a new release :+1: (in progress…)
If it still doesn’t work, in the meantime, you can already enter your Apple TV’s IP directly in the config tab.

We’re making progress, but this time I have another error:

The selector could be a bit more unique than that ^^

Codex still has some work to do :joy:

I may have been a bit « cheap » with the reasoning used haha
But it’s true that he didn’t bother for this one :joy:

New release available, I asked him to self-review and fix other potential issues of the same style!

Don’t hesitate to let me know if you encounter other issues if needed

I was able to add the Apple TV this time :slight_smile:

However, the device name could contain « Apple TV », because right now it’s just the room name (it must be the name given by Apple to my Apple TV), and so it’s not very readable:

Otherwise, I tried turning on the Apple TV with the « Power » type, but it does nothing:

The logs show nothing special:

[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

Does it work for you?

Attention, your integration has been listed on the integrations store even though it’s not functional yet. The idea is to only publish it once it’s operational. :wink:

In the meantime, you can remove the GitHub topic. This will prevent users from discovering it too early and being disappointed during their tests. :slightly_smiling_face:

Oops… I retracted! Codex added the tag without telling me! Sorry

I’m looking to fix it, but yes, Power was working for me last night.

Hi everyone!

Well… I’ve completely « rewritten » it from scratch with Claude Opus 5, it’s not a patch on the old version, it’s a new implementation.

What’s Different in the Approach

The old version went through node-pyatv, a Node wrapper that calls the CLI atvremote as a subprocess. It works for sending a key, but you lose everything else: each command restarts a process, each Apple TV session takes one to two seconds to open, and real-time updates don’t exist, they only live as long as the connection is held.

The new version includes a Python bridge that talks directly to pyatv. The Node process keeps the Gladys side (SDK, devices, actions) and drives a persistent Python worker via a pipe, in JSON lines. Concretely:

  • a session kept open permanently by Apple TV → the buttons respond immediately, and the state (play/pause, what’s on screen, the current app, the volume) is pushed back, not polled;
  • the entire pyatv API is accessible, not just what the CLI exposes: pairing of the two protocols, detection of the device’s real capabilities, listeners, list of installed applications;
  • pairing is done from the configuration screen. tvOS 15+ requires two sets of credentials — AirPlay then Companion — so two codes displayed on the TV. The integration chains the two steps by itself, and if a code expires (the connection that carries it closes after a minute) it displays a new one instead of leaving you stuck.

What It Looks Like in Gladys

  • A clickable remote control on a dashboard (PR #2765): directional cross, OK, Back, Home, Control Center, play/pause, transport keys, volume slider. Add a « Devices » box and choose your keys.
  • The Music box to control playback like a Sonos.
  • Text sensors « Current Playback » and « Application ».
  • A shortcut per installed application, so a scene can open Netflix or Disney+ directly.
  • Power, standby, and automatic discovery via AirPlay announcements on the network.

Conditional features (volume, power, applications) are only published once a real connection has proven that the device supports them, no volume slider that will never move.

Tested on an Apple TV 4K (3rd generation) running tvOS 26: pairing, remote control, applications, real-time state feedback.

You Can Test It

The development image is public:

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

One thing to know: if you’re running Gladys in Docker on a Mac or Windows, automatic discovery can’t work, because for example Docker Desktop, OrbStack, etc. run containers in a Linux VM, so network_mode: host refers to the VM’s network and the Apple TV’s multicast never arrives. Unicast works fine: enter your Apple TV’s IP address in « Manual IPv4 Addresses » and everything else works normally. On a Gladys installed on Linux (Raspberry Pi, NAS, server), nothing to do.

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

It’s not yet published in the store, I prefer to gather feedback first. Don’t hesitate!

Thanks for this new version, it’s much better, and this time it works on the first try. :slightly_smiling_face:

I still have a few small pieces of feedback on the UX. At first, I didn’t understand that I needed to start the pairing process, or that I had to retrieve the IP address of my Apple TV before I could do it.

In the Configuration tab, it’s possible to add a device selector before executing an action, and I think that’s exactly the right use case here. It would guide the user much better.

The text could also be slightly revised to explain the overall configuration process more clearly.

Oh, and a small funny bug: if I use the Left or Right buttons on the Apple TV home screen, it skips two apps with each movement instead of one. :grinning_face_with_smiling_eyes:

That also reminds me that we really need to develop a proper widget for this type of device. The list of dozens of features isn’t very pleasant to use. :sweat_smile: But in the meantime, it’s already a huge improvement, and most importantly, it finally allows creating scenes!

In any case, thanks a lot for the development, it’s a great addition!

Thanks for the detailed feedback, everything is fixed :wink: Don’t hesitate to re-test and give me your final comments before making this available on the store!

The device selector: I had tried initially, but the core validated the action fields against field.options, which a select with source: "devices" does not have, so « must be one of » and an empty list, no matter what you send. I left a comment in the code to explain why I fell back to a text input. It has been working since 4.85.0, so the seven actions now take a dropdown list and no one has to look up their IP address anymore. The integration therefore requires Gladys >= 4.85.0.

The texts: rewritten around the full sequence, with the real names of the UI buttons: an intro section that goes through Scan → Add to Gladys → pair → Update, then a section per settings block, and a final one just above the action buttons. The pairing completion message explicitly points to « Update » instead of vaguely saying to accept the update.

The two-app jump: well spotted, and the culprit isn’t where I expected. I checked: no one sends the key twice, neither Gladys, nor the front, nor my bridge, nor pyatv. In fact, tvOS does not read a directional key as an event but as a gesture, it measures the interval between the key-down and key-up HID and passes it to a tap or long-press recognizer (pyatv#792). In MRP, both go through the AirPlay tunnel, and this latency is enough for the home screen to believe in a held key and auto-repeat.

Navigation keys now go to Companion, the protocol that the clickable crown of a real Siri Remote speaks, instead of MRP that the pyatv facade chooses by default. Media commands remain on MRP, which has real playback semantics.

I couldn’t confirm on a TV before publishing, so if you can tell me if the focus moves well from one app now, that would help.

The widget: completely agree, the list of features is not a remote control. That’s on the core side and I can’t do anything from the integration. Meanwhile, we can disable app shortcuts in the config, which already removes up to 25 lines.

All of this is on the dev image: ghcr.io/valentinhttr/gladys-apple-tv:dev

The integration will be available in the store in about 1 hour as it seems quite stable, especially for creating scenes!

It is bound to evolve, so don’t hesitate to give me your feedback here directly :slight_smile:

Hello,

Thanks for the integration
I am on my side on a separate network between Gladys and my Apple TV
I have therefore entered the IP of the Apple TV and opened all the ports that I saw blocked between my 2 networks
Airplay is well activated and I tested by putting on the Apple TV « Access for everyone »
My Gladys installation has well « network mode: host »
But the search finds nothing
I also noticed that the integration tried to scan a few IP addresses around the IP of my Apple TV

Here are the 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

Any ideas? :slight_smile:

Hello,

Thank you for your feedback!
According to GPT 5.6 Sol xHigh, here is the cause:

The cause is a real limitation of the 1.0.0 integration, not a user misconfiguration.
The manual address is added to the candidates in discovery.js, then passed to pyatv.scan(hosts=…) in pyatv_bridge.py. However, this operation remains a direct mDNS request over UDP 5353. pyatv explicitly states that it does not work between subnets, as the Apple TV ignores requests from another network link. This is in line with the pyatv documentation and Section 5.5 of RFC 6762.

The network_mode: host does not change the separation between VLANs. „Access to everyone“ concerns AirPlay authorization, not the mDNS response. Opening UDP 5353 is also not enough when the request retains a source address located in the other subnet.

The fix requires an evolution on the Gladys side, as the integration container is on a Docker bridge network and does not receive the mDNS multicast. Only the Gladys core, placed on the host network, can capture these announcements.

Currently, the mediated discovery only transmits a single mDNS service declared in the manifest. However, pyatv needs at least the _airplay._tcp and _companion-link._tcp announcements to retrieve the identifiers, TXT properties, and dynamic ports. Otherwise, the integration attempts to request them directly from the Apple TV, but it ignores this request when it comes from another subnet.

@Pierre-Gilles, would it be possible for Gladys to allow declaring and retrieving multiple mDNS services for the same integration?

Otherwise, do you have any other ideas?