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.
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
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…
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.
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
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
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.
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:
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