ATV4 - Apple TV Binding

You will see lag when using autodiscover, but it should be quite fast if you specify address and login_id manually when running a command. pyatv is not that heavy, so it shouldn’t put much load on the system.

1 Like

Hi Postlund,

the goal is to use the data provided to set the values via REST API (https://docs.openhab.org/configuration/restdocs.html and http://demo.openhab.org:8080/doc/index.html) to items.

The push updates API sends the following data:

  • Media type
  • Play state
  • Title
  • Position
  • Repeat
  • Shuffle

An example:
If my wife starts playing a movie (or if possible pushes the button of the remote to activate apple tv) in the evening the state could be sent via API and openHAB could react on this change. Then it decides due to the luminance to switch on dimmed lights (for tv; a small lamp is already on due to motion). If she pauses the movie the lights are dimmed up to brightness. If play state is set to “Idle” and nothing happens for more than X minutes, then everything is switched off.

Another example:
My wife likes shows like “The good wife”. Sometimes there are several weeks between watching sessions. So she (and the apple tv) forgets about what she saw the last time. If the title and position could be sent to openhab to other fields, openHAB can react saving that. And she can look in her app what she watched the last time.

So what would be great if you could develop it?

  • Listener who reacts on a change of each of the listed (above) values
  • Reaction is sending an API request to openHAB
  • Some examples for GET, POST, PUT, DELETE with Python

Being flashed from your propose and sending thanks in advance!
HFM

Hi,
super! works smoothly!
thanks for the help!

Kurt

1 Like

The REST-part is not really an expertise of mine, so I can provide some conceptual code for that but you will have to adjust it to include the correct payload and select a proper endpoint. I’m only used to Home Assistant (that’s what I developed pyatv for) and have never used OpenHAB.

A very basic example could look like this:

"""Simple push update listener example."""

import sys
import asyncio
import aiohttp
from pyatv import helpers


class PushListener:

    def __init__(self, session):
        self.session = session

    async def _post(self, playstatus):
        # This should correspond to the json data expected by the API
        payload = {
            'title': playstatus.title
        }

        # Change address to some API endpoint
        async with self.session.post(
            'http://somehost/api/test', json=payload) as resp:
            # This just prints the response - do something useful here
            print('Response: ', await resp.text())

    def playstatus_update(self, _, playstatus):
        asyncio.ensure_future(self._post(playstatus))

    def playstatus_error(self, updater, exception):
        print('An error occurred (restarting): {0}'.format(exception))
        updater.start(initial_delay=1)


async def _listen_for_push_updates(atv):
    print('Starting to listen for updates')
    try:
        with aiohttp.ClientSession() as session:
            atv.push_updater.listener = PushListener(session)
            await atv.push_updater.start()
    except Exception as ex:
        print('Got an error: ', ex)
    finally:
        await atv.logout()

async def _no_device_found():
    print('No Apple TV found', file=sys.stderr)


if __name__ == '__main__':
    helpers.auto_connect(_listen_for_push_updates,
                         not_found=_no_device_found)

You will have to adjust the code in _post to do what you want. Also, if you want to POST to multiple endpoints, you can quite easily refactor to accomplish that. Multiple

Hi postlund,

thanks for your work!

I tried 2 days to get it working but I failed. I am too unexperienced with Python. I think I have no clue from the basic concepts.

What I tried

  • I tried to get a manual connect (instead autoconnect from helpers), that was a success, but then I put _listen_for_push_updates as second param and got lots of errors
  • Because I did not find out how to send a body and not json, I tried to include another library (python-openhab; https://pypi.python.org/pypi/python-openhab/2.2) to simplify the communication with openHAB; I got a first result but nothing happened on openHAB

Maybe you could assist a little bit?

Bye
HFM

Hi postlund,

I got something working - probably not nice, but - as I said - I have no experience… :slight_smile:
But I have one problem I think you can help me very easily.

Here is the code:

#! /usr/local/bin/python3.6
"""Simple push update listener example."""

import sys
import asyncio
import aiohttp
import pyatv
import requests
from pyatv import helpers

NAME = 'Apple TV (WoZi)'
ADDRESS = '192.168.13.180'
HSGID = '00000000-5110-4664-648c-edc64667425e'
DETAILS = pyatv.AppleTVDevice(NAME, ADDRESS, HSGID)
OHHOST = '192.168.13.222'
OHPORT = '8080'

class PushListener:

def __init__(self, session):
    self.session = session

    self.url = "http://%s:%s/rest" % (OHHOST,OHPORT)
    self.command_headers = {"Content-type": "text/plain"}
    self.polling_headers = {"Accept": "application/json"}

async def _post(self, playstatus):
    # This should correspond to the json data expected by the API
    #payload = {
    #    'title': playstatus.title
    #}
    #command = 'Test'

    # Change address to some API endpoint
    #async with self.session.post(
     #   self.url + '/Test_String', data=command) as resp:
      #  # This just prints the response - do something useful here
       # print('Response: ', await resp.text())
    #print('PLAYSTATUS ' + str(playstatus))
    #sendStatusToItem(self, 'TestMessage', str(playstatus.title))

    print("PLAYSTATE" + str(playstatus.play_state))
    print("MEDIATYPE" + str(playstatus.media_type))

    atvPlayState = playstatus.play_state
    atvPlayStateLabel = "AUS"
    atvPlayStateMediaStatus = playstatus.media_type
    

    if atvPlayState == 0:
        sendCommandToItem(self, 'Scene_Living_1', '3')
        atvPlayStateLabel = "AUS"

    elif atvPlayState == 3:
        sendCommandToItem(self, 'Scene_Living_1', '3')
        atvPlayStateLabel = "PAUSE"

    elif atvPlayState == 4 or atvPlayState == 6:
        sendCommandToItem(self, 'Scene_Living_1', '1')
        atvPlayStateLabel = "SPIELT"
        
    sendStatusToItem(self, 'TestMessage', str(atvPlayStateLabel) + ': ' + str(playstatus.title))
    #sendCommandToItem(self, 'Test_Button', 'ON')

    print (" ")
    print (" ")

def playstatus_update(self, _, playstatus):
    asyncio.ensure_future(self._post(playstatus))

def playstatus_error(self, updater, exception):
    print('An error occurred (restarting): {0}'.format(exception))
    updater.start(initial_delay=1)


def dump(obj):
    for attr in dir(obj):
        if hasattr( obj, attr ):
            print( "obj.%s = %s" % (attr, getattr(obj, attr)))


def sendCommandToItem(self, ohItem, command):
    print('STATUS')
    print('ITEM: ' + ohItem)
    print('COMMAND: ' + command)
    requestUrl = self.url+"/items/%s" % (ohItem)
    print('REQUEST: ' + requestUrl)
    req = requests.post(requestUrl, data=command, headers=self.command_headers)
    print('REQ: ' + str(req))

    return req.status_code


def sendStatusToItem(self, ohItem, command):
    print('STATUS')
    print('ITEM: ' + ohItem)
    print('COMMAND: ' + command)
    requestUrl = self.url+"/items/%s/state" % (ohItem)
    print('REQUEST: ' + requestUrl)
    req = requests.put(requestUrl, data=command, headers=self.command_headers)
    print('REQ: ' + str(req))

    return req.status_code

async def _listen_for_push_updates(atv):
    print('Starting to listen for updates')
    try:
        with aiohttp.ClientSession() as session:
            atv.push_updater.listener = PushListener(session)
            await atv.push_updater.start()
    except Exception as ex:
        print('Got an error: ', ex)
    finally:
        await atv.logout()

async def _no_device_found():
    print('No Apple TV found', file=sys.stderr)


if __name__ == '__main__':

    print('Connecting')
    #helpers.auto_connect(_listen_for_push_updates,
    #                    not_found=_no_device_found)
    atv = pyatv.connect_to_apple_tv(DETAILS, _listen_for_push_updates)

I am ready sending items (update and status) to openhab but I do not know how to connect direct to a specified apple tv. With the helpers method everything is fine, but because I have two apple tv I have to switch off the wrong one.

Maybe you could have a look on the last line?

Thanks!
HFM

P.S.: I have another question regarding pyatv. Is it possible to find out where the focus is in menu? So that I know where to start with the commands.

Hi!

Sorry for the delay. An example of doing manual connects is available here:

It’s a bit different when dealing with push updates though, I should add a better example of that. The name connect_to_apple_tv is a bit misleading and I should probably change that someday to something better (it doesn’t really connect, it only prepare everything so it’s ready).

Something like this should probably work:

if __name__ == '__main__':

    print('Connecting')
    loop = asyncio.get_event_loop()
    atv = pyatv.connect_to_apple_tv(DETAILS, loop)
    atv.push_updater.listener =  _listen_for_push_updates
    loop.run_until_complete(atv.push_updater.start())

I would recommend that you do not use the requests library as that blocks the async event loop, you should look at aiohttp that I used in the example instead. Also, you have the “play state” values in pyatv.const, so you can import that module and use those constants instead of magic numbers:

from pyatv import const
if atvPlayState == const.PLAY_STATE_PLAYING:
    ...

You have all the values here: https://github.com/postlund/pyatv/blob/v0.3.9/pyatv/const.py

Unfortunately it’s not possible to get any information regarding where in the menus you are with pyatv as that is not supported by the protocol. So it’s not possible to implement. Navigating blindly is the only way I’m afraid.

1 Like

I have successfully installed python 3.5.5 and pyatv, I have control over apple TV using CLI from the same machine running openhab2.2.

I can run:

sudo -u openhab atvremote --login_id 00000000-4e26-ec01-5ee2-43b0fd634ee1 --address 192.168.0.32 play

No problems and it works - however I can NOT get exec binding to trigger this without getting error:

Cannot run program “/home/stu/atvremote”: error=2, No such file or directory

My things file looks like this:

Thing exec:command:atv [command="/home/stu/atvremote --login_id 00000000-4e26-ec01-5ee2-43b0fd634ee1 --address 192.168.0.32 menu", interval=0, timeout=5]

My items file looks like this:

Switch ATV_ON { channel=“exec:command:atv:run”}

any ideas?

Stu

1 Like

OK - sorry! I decided to see where atvremote is on my system - /usr/local/bin/

changed things file to:

Thing exec:command:atv [command="/usr/local/bin/atvremote --login_id 00000000-4e26-ec01-5ee2-43b0fd634ee1 --address 192.168.0.32 menu", interval=0, timeout=5]

and it works!!

BUT i’m still a noob! I get the following error in my log:

13:38:10.504 [WARN ] [e.core.transform.TransformationHelper] - Cannot get service reference for transformation service of type REGEX
13:38:10.507 [WARN ] [nhab.binding.exec.handler.ExecHandler] - Couldn’t transform response because transformationService of type ‘REGEX’ is unavailable

and the switch item is thrown very quickly in my sitemap and reverts back to the off state imediatley.

Does anyone else use pyatv to bring apple tv out of standy and even better back into standy?

This would be great as the apple tv can bring my tv out of standy and back into standby with these commands

is it because I am using the command ‘run’ in the wrong way?

and finally - my wishlist! can i get pyatv to open a particular app such as plex or BBC iPlayer on apple tv?

S

Hi,
this would be awsome and on my same wishlist. The way I solved this, is by sending a “Menu” command to the ATV4 first, so the cursor would be sitting at the top-right app on the main menue. From there (since I know where my app, I’d like to start sits on that same menue), I am sending simple down,down,up commands, so I can start i.e. Netflix. Of course, this is not optimal, since the feedback loop is missing:

    executeCommandLine("atvremote --address 192.xxx.x.xxx --login_id 00000000-xxxx-xxxx-xxxx-xxxxxxxxxxxx menu")
    executeCommandLine("atvremote --address 192.xxx.x.xxx --login_id 00000000-xxxx-xxxx-xxxx-xxxxxxxxxxxx right right down select select")    
	logInfo("APPLE Remote", "Netflix Sequence executed")

The above commands work for me. I’m (as you can see above) in full noob-level as well, but maybe it can help.

Kurt

1 Like

fyi

I installed Python 3.6 and then tried to install the pyatv package

pip3.6 install pyatv

which results in

    gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -DUSE__THREAD -DHAVE_SYNC_SYNCHRONIZE -I/usr/include/ffi -I/usr/include/libffi -I/usr/local/include/python3.6m -c c/_cffi_backend.c -o build/temp.linux-armv7l-3.6/c/_cffi_backend.o
    c/_cffi_backend.c:15:17: fatal error: ffi.h: No such file or directory
     #include <ffi.h>
                     ^
    compilation terminated.
    error: command 'gcc' failed with exit status 1
    
    ----------------------------------------
Command "/usr/local/bin/python3.6 -u -c "import setuptools, tokenize;__file__='/tmp/pip-build-36j8796g/cffi/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-xlz06l5h-record/install-record.txt --single-version-externally-managed --compile" failed with error code 1 in /tmp/pip-build-36j8796g/cffi/
You are using pip version 9.0.1, however version 18.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.

as well as

atvremote scan

Traceback (most recent call last):
  File "/usr/local/bin/atvremote", line 7, in <module>
    from pyatv.__main__ import main
  File "/usr/local/lib/python3.6/site-packages/pyatv/__init__.py", line 12, in <module>
    from pyatv.pairing import PairingHandler
  File "/usr/local/lib/python3.6/site-packages/pyatv/pairing.py", line 11, in <module>
    import netifaces
ModuleNotFoundError: No module named 'netifaces'

which could be fixed by installing another dev package:

apt-get install libffi-dev
pip3.6 install netifaces

After installing pyatv it was able to find an Apple-TV 3 (old one, latest firmware) and also the pairing works.

However if I try to execute any command it failes

atvremote -a menu
or
atvremote --address 192.168.X.XXX --login_id 0xC4B869121XXXXXX menu

results in

/usr/local/lib/python3.6/site-packages/pyatv/internal/apple_tv.py:509: RuntimeWarning: coroutine 'ClientSession.close' was never awaited
  self._session.close()
ERROR: Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x75883690>
ERROR: Unclosed connector
connections: ['[(<aiohttp.client_proto.ResponseHandler object at 0x75879a40>, 568862.908645703)]']
connector: <aiohttp.connector.TCPConnector object at 0x75883610>

@postlund any idea?

It seems that some of the command gets execute before the client disconnects

WARNING: Unknown data: b'ceQu\x00\x00\x00\x01\x00ceMQ\x00\x00\x00\x01'
WARNING: Unknown data: b'ceMQ\x00\x00\x00\x01\x00ceNQ\x00\x00\x00\x04'
WARNING: Unknown data: b'ceNQ\x00\x00\x00\x04\x00\x00\x00\x00canp\x00\x00\x00\x10'
WARNING: Unknown data: b'canp\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00capl\x00\x00\x00 '
WARNING: Unknown data: b'capl\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00T\xe3\xf4\xb1cann\x00\x00\x00\x0f'
Media type: TV
Play state: Loading
     Title: Dreamer-Status
     Genre: Drama
    Repeat: Off
   Shuffle: False
/usr/local/lib/python3.6/site-packages/pyatv/internal/apple_tv.py:509: RuntimeWarning: coroutine 'ClientSession.close' was never awaited
  self._session.close()
ERROR: Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x75888650>
ERROR: Unclosed connector
connections: ['[(<aiohttp.client_proto.ResponseHandler object at 0x7587ea40>, 569942.582767109)]']
connector: <aiohttp.connector.TCPConnector object at 0x758885d0>

Media type: TV
Play state: Loading
     Title: Dreamer-Status
     Genre: Drama
    Repeat: Off
   Shuffle: False
matches the current status.

What's about those WARNINGs?

I have the same or a similar problem (running on RasPI). Seems that commands do work (slowly though) but session closing doesn’t. That generates those errors. I don’t know Python enough (yet) to fix that.

I gave it a try based on rules running atvremote on the command line and ignoring the errors and it works fine. Using a HABpanel dashboard and a button item I’m sending the desired key and the appropriate rule gets triggered. The only “problem” is that for every key it needs 5-7s to execute the command, which makes it slow when pressing buttons one after the other, but you could run multiple commands at once (see rule for SERIES).

rule "ATV wakeup"
when
	Item ATV_Key received command "POWER"
then
	logInfo("APPLETV", "Wakeup Appl-TV")
	executeCommandLine("/usr/local/bin/atvremote@@--address@@" + atv_ip + "@@--login_id@@" + atv_loginid + "@@top_menu", 5000)
end

rule "ATV MENU key"
when
	Item ATV_Key received command "MENU"
then
	logInfo("APPLETV", "Press MENU key")
	executeCommandLine("/usr/local/bin/atvremote@@--address@@" + atv_ip + "@@--login_id@@" + atv_loginid + "@@menu", 5000)
end

rule "ATV SELECT key"
when
	Item ATV_Key received command "ENTER"
then
	logInfo("APPLETV", "Press SELECT key")
	executeCommandLine("/usr/local/bin/atvremote@@--address@@" + atv_ip + "@@--login_id@@" + atv_loginid + "@@select", 5000)
end

rule "ATV PLAY key"
when
	Item ATV_Key received command "PLAY"
then
	logInfo("APPLETV", "Press PLAY key")
	executeCommandLine("/usr/local/bin/atvremote@@--address@@" + atv_ip + "@@--login_id@@" + atv_loginid + "@@play", 5000)
end

rule "ATV PAUSE key"
when
	Item ATV_Key received command "PAUSE"
then
	logInfo("APPLETV", "Press PAUSE key")
	executeCommandLine("/usr/local/bin/atvremote@@--address@@" + atv_ip + "@@--login_id@@" + atv_loginid + "@@pause", 5000)
end

rule "ATV PAUSE key"
when
	Item ATV_Key received command "PAUSE"
then
	logInfo("APPLETV", "Press PAUSE key")
	executeCommandLine("/usr/local/bin/atvremote@@--address@@" + atv_ip + "@@--login_id@@" + atv_loginid + "@@pause", 5000)
end

rule "ATV PREV key"
when
	Item ATV_Key received command "PREV"
then
	logInfo("APPLETV", "Press PREV key")
	executeCommandLine("/usr/local/bin/atvremote@@--address@@" + atv_ip + "@@--login_id@@" + atv_loginid + "@@previous", 5000)
end

rule "ATV NEXT key"
when
	Item ATV_Key received command "NEXT"
then
	logInfo("APPLETV", "Press NEXT key")
	executeCommandLine("/usr/local/bin/atvremote@@--address@@" + atv_ip + "@@--login_id@@" + atv_loginid + "@@next", 5000)
end

rule "ATV UP key"
when
	Item ATV_Key received command "UP"
then
	logInfo("APPLETV", "Press UP key")
	executeCommandLine("/usr/local/bin/atvremote@@--address@@" + atv_ip + "@@--login_id@@" + atv_loginid + "@@up", 5000)
end

rule "ATV DOWN key"
when
	Item ATV_Key received command "DOWN"
then
	logInfo("APPLETV", "Press DOWN key")
	executeCommandLine("/usr/local/bin/atvremote@@--address@@" + atv_ip + "@@--login_id@@" + atv_loginid + "@@down", 5000)
end

rule "ATV LEFT key"
when
	Item ATV_Key received command "LEFT"
then
	logInfo("APPLETV", "Press LEFT key")
	executeCommandLine("/usr/local/bin/atvremote@@--address@@" + atv_ip + "@@--login_id@@" + atv_loginid + "@@left", 5000)
end

rule "ATV RIGHT key"
when
	Item ATV_Key received command "RIGHT"
then
	logInfo("APPLETV", "Press RIGHT key")
	executeCommandLine("/usr/local/bin/atvremote@@--address@@" + atv_ip + "@@--login_id@@" + atv_loginid + "@@right", 5000)
end

rule "ATV SERIES"
when
	Item ATV_Key received command "SERIES"
then
	logInfo("APPLETV", "Jump to SERIES selection")
	executeCommandLine("/usr/local/bin/atvremote@@--address@@" + atv_ip + "@@--login_id@@" + atv_loginid + "@@top_menu@@left@@left@@left@@left@@right@@select@@left@@left@@select", 5000)
end

I’m thinking to build a binding wrapping the Python code, but I have no clue about Python. I know that there is Jython, which allows to call Phyton code from Java, but @postlund would be very helpful to get this together. This would also allow to intercept the events.

1 Like

any update here? Nobody else interested in an Apple-TV binding?

1 Like

I am interested. I am not a coder. I have a gen 4 and a gen 2 ATV.

I am . just waiting on my Gen 4

It’s not a binding but it’s been working for me - I did a write up of my setup with the above mentioned pyatv.

I had some errors with home sharing previously but the recent updates to the release_0_3_x branch have made pairing work much better than before - I recommend pairing. I’ve updated my original walkthrough (linked below) to show pairing.

Install with:

pip3 install git+https://github.com/postlund/pyatv.git@release_0_3_x

What I did:

1 Like

I am unable to return artwork using the exec binding. When trying to display as image it displays the string value only. Is anyone else having this issue?

fyi: I started the development of a Apple-TV binding based on pyatv
check here for more information: [NEW] Binding for Apple-TV

1 Like