Binding for Apple-TV

@magnuslsjoberg this can be a great solution! Can you explain the path you followed to integrate pyatv in HABApp?

@Rickytr

HABApp

  1. Follow habappā€™s installation instructions and install habapp in a virtual python environment.
  2. Implement a habapp test rule and verify installation.
  3. Set up habapp to start at boot.

pyatv

  1. Activate the habapp virtual environment.
  2. Install pyatv in that environment following the instructions for pyatv.
  3. Run ā€œatvremoteā€ to verify the setup.

HABApp / pyatv

Please note that this is a only simple way to get it to work! I used the ā€˜atvremoteā€™ code as
inspiration but didnā€™t understand all of it. I am certain that someone with greater python skills
could integrate this in a better way. The code below is trimmed down but should work.

NOTE! The indentation got messed up when I paste the code here (donā€™t know what Iā€™m doing wrong)

import HABApp
from HABApp.openhab.events import ItemCommandEvent
import logging
import asyncio
import pyatv

class AppleTV(HABApp.Rule):

    def __init__(self):
        super().__init__()

    # Get the event loop for asyncio
    self.loop = HABApp.core.const.loop

    # No Apple TV connected yet
    self.atv = None

    # Listen to control commands from openHAB (from proxy String AppleTV_Control)
    self.listen_event( 'AppleTV_Control', self.control, ItemCommandEvent )

    # Scan and connect
    self.run_soon(self.connect)

    # Poll status every 5s
    self.run_every(None,5,self.status)

async def connect(self):
    log.info("Discovering devices on network...")
    atvs = await pyatv.scan(self.loop, timeout=20)
    if atvs :
        # Only got one AppleTV
        log.info("Connecting to {0}".format(atvs[0].address))
        self.atv = await pyatv.connect(atvs[0],self.loop)
        log.info(self.atv.device_info)
    else :
        log.error("No device found")

def control(self,event):
    assert isinstance(event, ItemCommandEvent), type(event)
    if self.atv :
        # Let the async function 'handle_command' handle this later
        self.run_soon(self.handle_command, str(event.value))    

async def handle_command(self,command) :
    # In 'atvremote' this is much more elegant!
    if command == 'select' :
        await self.atv.remote_control.select()
    # elif ...
    #... list all commands handled
    elif command == 'home_hold' :
        await self.atv.remote_control.home(action=pyatv.const.InputAction.Hold)

async def status(self) :
    if self.atv :
        status = await self.atv.metadata.playing()
        log.info(f'Playing:\n{status}')
        
        app = self.atv.metadata.app
        log.info(f'App: {app}') 


# Rule initialization      
log = logging.getLogger('HABApp')
AppleTV()
2 Likes

Great guide @magnuslsjoberg! Thanks a lot.

@Rickytr You are welcome!

I find both HABApp and pyatv very robust and both projects have excellent documentation.

If you find a way to handle the commands better (instead of listing all of them) or if you
find out how to get push updates from pyatv rather than polling, please report here!

Hi markus7017 and thank you very much for all your work!
Is there any way to get this working with OH3?
Thank you.

1 Like

Hi everyon,
+1

@magnuslsjoberg I tried to follow your instructions and I can run HABApp from commandline and it is loading the rules files, but I get some errors and have no idea where to start:

[2021-02-23 10:45:17,687] [             HABApp.Rules]    ERROR | Error "name 'self' is not defined" in load:
[2021-02-23 10:45:17,688] [             HABApp.Rules]    ERROR | Could not load /etc/openhab/habapp/rules/test.py!
[2021-02-23 10:45:17,688] [             HABApp.Rules]    ERROR | File "/opt/habapp/lib/python3.7/site-packages/HABApp/rule_manager/rule_file.py", line 79, in load
[2021-02-23 10:45:17,688] [             HABApp.Rules]    ERROR |     self.create_rules(created_rules)
[2021-02-23 10:45:17,688] [             HABApp.Rules]    ERROR | File "/opt/habapp/lib/python3.7/site-packages/HABApp/rule_manager/rule_file.py", line 68, in create_rules
[2021-02-23 10:45:17,689] [             HABApp.Rules]    ERROR |     '__HABAPP__RULES': created_rules,
[2021-02-23 10:45:17,689] [             HABApp.Rules]    ERROR | File "/usr/lib/python3.7/runpy.py", line 263, in run_path
[2021-02-23 10:45:17,689] [             HABApp.Rules]    ERROR |     pkg_name=pkg_name, script_name=fname)
[2021-02-23 10:45:17,689] [             HABApp.Rules]    ERROR | File "/usr/lib/python3.7/runpy.py", line 96, in _run_module_code
[2021-02-23 10:45:17,690] [             HABApp.Rules]    ERROR |     mod_name, mod_spec, pkg_name, script_name)
[2021-02-23 10:45:17,690] [             HABApp.Rules]    ERROR | File "/usr/lib/python3.7/runpy.py", line 85, in _run_code
[2021-02-23 10:45:17,690] [             HABApp.Rules]    ERROR |     exec(code, run_globals)
[2021-02-23 10:45:17,690] [             HABApp.Rules]    ERROR | File "/etc/openhab/habapp/rules/test.py", line 7, in test.py
[2021-02-23 10:45:17,691] [             HABApp.Rules]    ERROR |     3    import logging
[2021-02-23 10:45:17,691] [             HABApp.Rules]    ERROR |     4    import asyncio
[2021-02-23 10:45:17,691] [             HABApp.Rules]    ERROR |     5    import pyatv
[2021-02-23 10:45:17,691] [             HABApp.Rules]    ERROR |     6    
[2021-02-23 10:45:17,692] [             HABApp.Rules]    ERROR | --> 7    class AppleTV(HABApp.Rule):
[2021-02-23 10:45:17,692] [             HABApp.Rules]    ERROR |     8    
[2021-02-23 10:45:17,692] [             HABApp.Rules]    ERROR |     ..................................................
[2021-02-23 10:45:17,692] [             HABApp.Rules]    ERROR |      logging = <module 'logging' from '/usr/lib/python3.7/logging/__init__.py'>
[2021-02-23 10:45:17,693] [             HABApp.Rules]    ERROR |      asyncio = <module 'asyncio' from '/usr/lib/python3.7/asyncio/__init__.py'>
[2021-02-23 10:45:17,694] [             HABApp.Rules]    ERROR |      pyatv = <module 'pyatv' from '/opt/habapp/lib/python3.7/site-packages/pyatv/__init__.py'>
[2021-02-23 10:45:17,694] [             HABApp.Rules]    ERROR |      HABApp.Rule = <class 'HABApp.rule.rule.Rule'>
[2021-02-23 10:45:17,694] [             HABApp.Rules]    ERROR |     ..................................................
[2021-02-23 10:45:17,694] [             HABApp.Rules]    ERROR | 
[2021-02-23 10:45:17,695] [             HABApp.Rules]    ERROR | File "/etc/openhab/habapp/rules/test.py", line 13, in AppleTV
[2021-02-23 10:45:17,695] [             HABApp.Rules]    ERROR |     9    def __init__(self):
[2021-02-23 10:45:17,695] [             HABApp.Rules]    ERROR |     10       super().__init__()
[2021-02-23 10:45:17,695] [             HABApp.Rules]    ERROR |     11   
[2021-02-23 10:45:17,696] [             HABApp.Rules]    ERROR |     12   # Get the event loop for asyncio
[2021-02-23 10:45:17,696] [             HABApp.Rules]    ERROR | --> 13   self.loop = HABApp.core.const.loop
[2021-02-23 10:45:17,696] [             HABApp.Rules]    ERROR |     14   
[2021-02-23 10:45:17,697] [             HABApp.Rules]    ERROR |     ..................................................
[2021-02-23 10:45:17,697] [             HABApp.Rules]    ERROR |      __init__ = <function 'AppleTV.__init__' test.py:9>
[2021-02-23 10:45:17,697] [             HABApp.Rules]    ERROR |      HABApp.core.const.loop = <_UnixSelectorEventLoop running=True closed=False debug=True>
[2021-02-23 10:45:17,697] [             HABApp.Rules]    ERROR |     ..................................................
[2021-02-23 10:45:17,697] [             HABApp.Rules]    ERROR | 
[2021-02-23 10:45:17,698] [             HABApp.Rules]    ERROR | NameError: name 'self' is not defined

When I try to set HABApp to run as a service, the log file does not show anything when I start it but that is a separate topic I guess.

Hi Ronny!

As I wrote in my post:

NOTE! The indentation got messed up when I paste the code here (donā€™t know what Iā€™m doing wrong)

The init should be:

    def __init__(self):
        super().__init__()

        # Get the event loop for asyncio
        self.loop = HABApp.core.const.loop

        # No Apple TV connected yet
        self.atv = None

        # Listen to control commands from openHAB (from proxy String AppleTV_Control)
        self.listen_event( 'AppleTV_Control', self.control, ItemCommandEvent )

        # Scan and connect
        self.run_soon(self.connect)

        # Poll status every 5s
        self.run_every(None,5,self.status)

Please try that!

@markus7017
+1 for oh3!

1 Like

Would be nice to see this binding built for Openhab 3!

2 Likes

Would be really nice to control the ATV through openhab!

Hi all-

Iā€™ve spent a little time looking at what needs to be done to get this binding working on OH3 and with newer ATV devices. I spent quite a bit of time trying to get jpy working and didnā€™t have much success. I think the limitations placed on us by OSGI and jpy make the problem unlikely to be easily solved.

However, pyatv includes a utility called atvscript, which I think can be used to avoid java-python inter-operation.

As a first step, I managed to get discovery working. I think pairing without running atvremote on the command line is going to be hard to do, but with good documentation that might not be a huge problem. The atvscript tool can also push updates to us in real-time, so thereā€™s actually an advantage versus polling as the current binding does.

Hopefully most of the binding code can simply be reused, which is always a plus. Not sure how much time Iā€™ll have to work on this, but if anyone is interested in testing, let me know!

Somehow the OH runtime includes jpy, but I never got to the point to get access to this instance. In general that shouldnā€™t be unsolvable, but you are right I also spent a lot of time on this. The reason for polling was just a lack of Python knowledge. The bindings brings a itā€™s own and modified copy of the older pyatv. The way to include external components in OH3 has also changed. The binding did itā€™s job for OH2, but I think a rework if the lower layer is required.

Using the command line tools and redirecting the output should be feasible. It did this once for my WhatsApp binding running youwsapp cli and this worked pretty well (I could re-open the repo if you are interested in the code). Let me know if you have questions in the existing code.

And yes, I could support testing, because I canā€™t control my ATV from OH anymore :wink:

Hi-

Thanks for your feedback! I appreciate that youā€™ve responded so quickly. I had a look through your code for the binding and it seems pretty reasonable. Generally speaking, I give my work freely to whoever wants to use it, so regardless of whether I succeed, the changes I try will be free for anyone to take.

I do like the idea of jpy, and (oddly enough) Iā€™ve got experience with java/native interaction. Unfortunately, I think that the restrictions OH3 have put in place mean that itā€™s maybe not worth trying. I am not a python expert but I think if (as the ultimate goal) the binding were to read the output of a python script that runs commands without having to start it each time, we can have all of the benefits of embedded python without the hassle of getting it to work. Thereā€™s support in pyatv for doing that with events, so I think any changes needed should be possible to send back to the pyatv folks.

Long story short, I have an idea of how this might be done, but I would be glad to look at your code for inspiration! I can keep you posted and once I have something that seems like it works, Iā€™ll push it somewhere you can have a look.

One other thing, it seems like pyatv can control other devices like HomePod minis and suchā€¦ was wondering if maybe there was a better name for the binding but am not sure I have a good idea. Something maybe to think about!

Hello all-

Iā€™ve been working on producing a version of this binding that works with OpenHAB 3 and have something that I think Iā€™d call a ā€œproof of concept.ā€ Itā€™s very early in the development phase, so I wouldnā€™t recommend this for the faint of heart. That said, the basic functionality seems to work, and there are a number of additional features beyond what the previous versions provided. Iā€™d love to get some feedback if anyone is willing to have a go.

Some details of whatā€™s new, changed and where problems likely lurk are in the README:

https://git.sr.ht/~hww3/org.openhab.binding.appletv

Iā€™ve also prepared a jar with the current version of the code, so the portion of the install dealing with building the binding can be skipped by downloading the pre-built jar from this download page:

https://git.sr.ht/~hww3/org.openhab.binding.appletv/refs/0.1.0

Youā€™ll still need to install the enhanced version of pyatv, as described in the instructions. Hopefully in the future that can be simplified as well.

Note: I donā€™t use item or thing configuration files myself, so any questions about how to set those up (vs using auto-discovery) will probably need to be answered by others.

As always, please feel free to get in touch with any comments, questions or suggestions.

1 Like

Hi there,

as iĀ“m now also a owner of an apple-tv, i just wonder what the current state is here?? IĀ“m very open to do some tests if needed. Does it make any sense to use the code from @hww3 ??

IĀ“m currently running openHAB 3.3.0.RC1 on Openhabian (raspberry) and have an ATV 4k, latest software-release. Let me know, how I can help, even IĀ“m not able to codeā€¦

cheers

Hi-

Since youā€™re running openHAB 3, the rewrite of the binding Iā€™m working on is your only option, as the previous version wonā€™t work at all. Iā€™m actively working on it, so if you do run into problems using it, I can likely fix them pretty quickly.

Iā€™m in the middle of adding support for using AppleTV devices as ā€œaudio sinksā€ so that you can play sounds or speech to them. That will probably be ready for testing in a week or two.

Sorry for delayed answer, I missed out your answer.
back to topic: So you recommend to use the .jar in my environment, cool!
Will do so and report back, might take a while, much other things to do in summer :wink:

Regarding ā€œAudio Sinkā€: Do you think this make sense? A ATV do not have own speakers (AFAIK), so you have anything additionally switched on and run the whole time to get the output heared. In my case it is a receiver, which consumes (for me) to much power to let it run 24/7. A Alexa/HomePod etc. consumes much less power, so it is for me the much better solution. Not sure, whether I got your use case for this??

Anyhow, I do not have a concrete use case for the ATV at all at the moment, just want to ā€œseeā€ it and probably switch it ON/OFF from my openhab-environment.

Yes, it may not be super helpful to use it as a default audio sink, but if there was a notification and you were actively using the ATV device (which is a condition you could check), you might want to route the audio to it.

Also, calling this the ā€œApple TVā€ binding is a bit misleading, as there are a number of devices that can potentially be controlled by it. Iā€™m looking at supporting HomePods and other devices that support AirPlay, and those could be a default audio destination. Getting that working reliably is going to take a bit of work, so itā€™s not going to happen immediately but I think itā€™s a nice option for the future.

Anyhow, feedback is always welcome and Iā€™ll try to keep those links updated with the latest versions!

1 Like

Hi, I just installed the most recent build with the current snapshot (3.4.0-SNAPSHOT - Build #3011). I had some trouble getting pyatv working, due to a library issue with miniaudio. For that, I had to install it from source, modifying the libs outlined here: _miniaudio.abi3.so: undefined symbol: __atomic_load_8 on raspbian Ā· Issue #52 Ā· irmen/pyminiaudio Ā· GitHub
I am only able to pair with the ā€œcompanionā€ protocol, and it wont receive any updates, giving the following error:

{
  "result": "failure",
  "datetime": "2022-07-20T15:14:33.403169-07:00",
  "exception": "power_state is not supported",
  "stacktrace": "Traceback (most recent call last):\n  File \"/usr/local/lib/python3.7/dist-packages/pyatv/scripts/atvscript.py\", line 452, in appstart\n    print(args.output(await _handle_command(args, abort_sem, loop)), flush=True)\n  File \"/usr/local/lib/python3.7/dist-packages/pyatv/scripts/atvscript.py\", line 256, in _handle_command\n    return await _run_command(atv, args, abort_sem, loop)\n  File \"/usr/local/lib/python3.7/dist-packages/pyatv/scripts/atvscript.py\", line 281, in _run_command\n    output(True, values={\"power_state\": atv.power.power_state.name.lower()})\n  File \"/usr/local/lib/python3.7/dist-packages/pyatv/support/shield.py\", line 72, in _guard_method\n    return func(self, *args, **kwargs)\n  File \"/usr/local/lib/python3.7/dist-packages/pyatv/core/facade.py\", line 338, in power_state\n    return self.relay(\"power_state\")\n  File \"/usr/local/lib/python3.7/dist-packages/pyatv/core/relayer.py\", line 91, in relay\n    target, chain(self._takeover_protocol, priority or self._priorities)\n  File \"/usr/local/lib/python3.7/dist-packages/pyatv/core/relayer.py\", line 114, in _find_instance\n    raise exceptions.NotSupportedError(f\"{target} is not supported\")\npyatv.exceptions.NotSupportedError: power_state is not supported\n"
}