Pairing with a device¶
An alternative to using home sharing and the HSGID
identifier is to pair
with the device and use a pairing guid
instead. This is useful if you
for some reason do not want to set up home sharing or if you do not have an
Apple account.
The library automatically handles both types of identifiers. Once you have one
of them, it should just work. So, from now on the term login id
will be
used, which can be either a HSGID or pairing guid.
How does it work¶
Pairing with a device is sort of a reversed process compared to when sending
commands to it. The Apple TV listens to a specific Bonjour service with type
_touch-remote._tcp.local.
and all remote controls publishes this service.
Included in this service is a bunch of data that is needed for the pairing
process:
IP-address of the host (remote control)
A TCP-port open on the host
Various properties, like the remote name and the pairing guid to be used
When pyatv publishes this service, it might look something like this:
ServiceInfo(type='_touch-remote._tcp.local.',
name='0000000000000000000000000000000000000001._touch-remote._tcp.local.',
address=b'\n\x00\n\x19',
port=57469,
weight=0,
priority=0,
server='0000000000000000000000000000000000000001._touch-remote._tcp.local.',
properties={
b'DvNm': b'pyatv',
b'txtvers': b'1',
b'RemV': b'10000',
b'Pair': b'0000000000000001',
b'DvTy': b'iPod',
b'RemN': b'Remote'
}
)
Note the port
, DvNM
(name of the remote) and Pair
. These are the
most interesting parameters.
When this service is published, a new remote will “popup” on the Apple TV. Next is the actual pairing, which is the tricky part. To avoid sending the pin code over the network (which would eliminate integrity all together), it instead generates a MD5 hash based on the Pair property and the PIN you manuelly entered on scren. In case of pyatv, the “algorithm” is:
hash = md5(Pair + pin)
It then connects to the port on the host (remote control) and performs a GET request, including this hash as well as a service name. An example might look like this:
INFO: 10.0.10.22 - - [04/Feb/2017:15:26:29 +0000] "GET /pair?pairingcode=AAB15DF9F73AA252A7934E0AF9C86B13&servicename=AAAAAAAAAAAAAAAA HTTP/1.1" 200 49 "-" "AppleTV/7.2.2 iOS/8.4.2 AppleTV/7.2.2 model/AppleTV3,1 build/12H606 (3; dt:12)"
The remote control then calculates the hash in the same way and verifies if it is correct. Of course, the remote decides if the pairing should succeed so this verification can be skipped altogether to allow any PIN code.
After verifying the hash, a response must be sent sent back to the device. It is DAAP data and looks like this:
cmpa: [container, dacp.pairinganswer]
cmpg: 1 [uint, dacp.pairingguid]
cmnm: pyatv remote [str, dacp.devicename]
cmty: ipod [str, dacp.devicetype]
As can be seen, the name to be used for the remote is included in the response.
The pairing guid represented as an integer (0000000000000001
-> 1
) is
also included in cmpg
. After this response, the pairing process is complete
and 0000000000000001
can be used as login id when sending commands to the
device.
Note
If a different pairing guid is used, that should be used as login_id
instead of 0000000000000001
(which is just an example).
Code Example: Pairing¶
When performing pairing, the application is responsible for starting and stopping
the process. In practice this means publishing the Bonjour service, starting the
web server and the opposite. This is done using a
pyatv.pairing.PairingHandler
, which is returned when you call
pyatv.pair_with_apple_tv()
. The process itself is quite simple:
import pyatv
import asyncio
from zeroconf import Zeroconf
PIN_CODE = 1234
REMOTE_NAME = 'my remote control'
@asyncio.coroutine
def pair_with_device(loop):
my_zeroconf = Zeroconf()
handler = pyatv.pair_with_apple_tv(loop, PIN_CODE, REMOTE_NAME)
# handler.pairing_guid = '1234ABCDE56789FF'
yield from handler.start(my_zeroconf)
yield from asyncio.sleep(60, loop=loop)
yield from handler.stop()
if handler.has_paired:
print('Paired with device!')
print('Pairing guid: ' + handler.pairing_guid)
else:
print('Did not pair with device!')
my_zeroconf.close()
loop = asyncio.get_event_loop()
loop.run_until_complete(pair_with_device(loop))
By default, a random pairing guid is generated. You can access it with
handler.pairing_guid
in order to present it to the user. To change the
pairing guid, you can change this variable to something else before calling
start
(see above).
This example is available in examples
.