Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Import of MRP prototype #114

Merged
merged 1 commit into from
Feb 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ source = pyatv

omit =
pyatv/__main__.py
pyatv/mrp/*.py # TODO: remove when MRP has tests
pyatv/mrp/protobuf/*.py

[report]
# Regexes for lines to exclude from consideration
Expand Down
82 changes: 63 additions & 19 deletions pyatv/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ def _print_commands(title, api):
print('{} commands:\n{}\n'.format(title, commands))


@asyncio.coroutine
def _read_input(loop, prompt):
sys.stdout.write(prompt)
sys.stdout.flush()
user_input = yield from loop.run_in_executor(None, sys.stdin.readline)
return user_input.strip()


class GlobalCommands:
"""Commands not bound to a specific device."""

Expand Down Expand Up @@ -94,6 +102,23 @@ def __init__(self, atv, loop, args):
self.loop = loop
self.args = args

@asyncio.coroutine
def cli(self):
"""Enter commands in a simple CLI."""
print('Enter commands and press enter')
print('Type help for help and exit to quit')

while True:
command = yield from _read_input(self.loop, 'pyatv> ')
if command.lower() == 'exit':
break
elif command == 'cli':
print('Command not availble here')
continue

yield from _handle_device_command(
self.args, command, self.atv, self.loop)

@asyncio.coroutine
def artwork_save(self):
"""Download artwork and save it to artwork.png."""
Expand All @@ -112,6 +137,7 @@ def push_updates(self):
print('Press ENTER to stop')

self.atv.push_updater.start()
yield from self.atv.login()
yield from self.loop.run_in_executor(None, sys.stdin.readline)
self.atv.push_updater.stop()
return 0
Expand All @@ -124,7 +150,8 @@ def auth(self):

try:
yield from self.atv.airplay.start_authentication()
pin = input('Enter PIN on screen: ')
pin = yield from _read_input(self.loop, 'Enter PIN on screen: ')

yield from self.atv.airplay.finish_authentication(pin)
print('You may now use these credentials:')
print(credentials)
Expand All @@ -141,7 +168,7 @@ def pair(self):
if protocol == const.PROTOCOL_DMAP:
yield from self._pair_with_dmap_device()
elif protocol == const.PROTOCOL_MRP:
raise exceptions.NotSupportedError()
yield from self._pair_with_mrp_device()

@asyncio.coroutine
def _pair_with_dmap_device(self):
Expand All @@ -150,23 +177,39 @@ def _pair_with_dmap_device(self):

yield from self.atv.pairing.start(
zeroconf=Zeroconf(), name=self.args.name, pin=self.args.pin_code)
pairing_guid = yield from self.atv.pairing.get('pairing_guid')

print('Use pin {0} to pair with "{1}" (press ENTER to stop)'.format(
self.args.pin_code, self.args.remote_name))
print('Using pairing guid: 0x{0}'.format(pairing_guid))
print('Note: If remote does not show up, try rebooting your Apple TV')

yield from self.loop.run_in_executor(None, sys.stdin.readline)
yield from self.atv.pairing.stop()

# Give some feedback to the user
if self.atv.pairing.has_paired:
credentials = yield from self.atv.pairing.get('credentials')
print('Pairing seems to have succeeded, yey!')
print('You may now use these credentials: 0x{}'.format(
credentials))
else:
print('Pairing failed!')
return 1

return 0

@asyncio.coroutine
def _pair_with_mrp_device(self):
yield from self.atv.pairing.start()
pin = yield from _read_input(self.loop, 'Enter PIN on screen: ')
yield from self.atv.pairing.stop(pin=pin)

# Give some feedback to the user
if self.atv.pairing.has_paired:
credentials = yield from self.atv.pairing.get('credentials')
print('Pairing seems to have succeeded, yey!')
print('You may now use this login id: 0x{}'.format(
pairing_guid))
print('You may now use these credentials: {0}'.format(credentials))
else:
print('No response from Apple TV!')
print('Pairing failed!')
return 1

return 0
Expand Down Expand Up @@ -245,12 +288,11 @@ def cli_handler(loop):
help='pairing guid (16 chars hex)',
dest='pairing_guid', default=None)

ident = parser.add_mutually_exclusive_group()
ident.add_argument('-a', '--autodiscover',
help='automatically find a device',
action='store_true', dest='autodiscover', default=False)
ident.add_argument('--login_id', help='home sharing id or pairing guid',
dest='login_id', default=None)
parser.add_argument('-a', '--autodiscover', action='store_true',
help='automatically find a device',
dest='autodiscover', default=False)
parser.add_argument('--device_credentials', help='credentials to device',
dest='device_credentials', default=None)

airplay = parser.add_argument_group('airplay')
airplay.add_argument('--airplay_credentials',
Expand Down Expand Up @@ -285,7 +327,7 @@ def cli_handler(loop):
return 1

return (yield from _handle_commands(args, loop))
elif args.login_id:
elif args.device_credentials:
return (yield from _handle_commands(args, loop))
else:
logging.error('To autodiscover an Apple TV, add -a')
Expand All @@ -311,7 +353,7 @@ def _autodiscover_device(args, loop):
return None
elif len(atvs) > 1:
logging.error('Found more than one Apple TV; '
'specify one using --address and --login_id')
'specify one using --address and --device-credentials')
_print_found_apple_tvs(atvs, outstream=sys.stderr)
return None

Expand All @@ -334,7 +376,7 @@ def _autodiscover_device(args, loop):

# Protocol specific parameters
if service.protocol == const.PROTOCOL_DMAP:
args.login_id = service.login_id
args.device_credentials = service.device_credentials

logging.info('Auto-discovered %s at %s', args.name, args.address)

Expand Down Expand Up @@ -363,9 +405,11 @@ def _extract_command_with_args(cmd):
def _handle_commands(args, loop):
details = AppleTV(args.address, args.name)
if args.protocol == const.PROTOCOL_DMAP:
details.add_service(DmapService(args.login_id, port=args.port))
details.add_service(DmapService(
args.device_credentials, port=args.port))
elif args.protocol == const.PROTOCOL_MRP:
details.add_service(MrpService(args.port))
details.add_service(MrpService(
args.port, device_credentials=args.device_credentials))

atv = pyatv.connect_to_apple_tv(details, loop, protocol=args.protocol)
atv.push_updater.listener = PushListener()
Expand Down Expand Up @@ -417,7 +461,7 @@ def _handle_device_command(args, cmd, atv, loop):
return (yield from _exec_command(
atv.airplay, cmd, True, *cmd_args))

logging.error('Unknown command: %s', args.command[0])
logging.error('Unknown command: %s', cmd)
return 1


Expand Down
19 changes: 13 additions & 6 deletions pyatv/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,14 @@ def __str__(self):
class DmapService(BaseService):
"""Representation of a DMAP service."""

def __init__(self, login_id, port=None):
def __init__(self, device_credentials, port=None):
"""Initialize a new DmapService."""
super().__init__(PROTOCOL_DMAP, port or 3689)
self.login_id = login_id
self.device_credentials = device_credentials

def is_usable(self):
"""Return True if service is usable, else False."""
return self.login_id is not None
return self.device_credentials is not None

def superseeded_by(self, other_service):
"""Return True if input service has login id and this has not."""
Expand All @@ -151,25 +151,32 @@ def superseeded_by(self, other_service):

# If this service does not have a login id but the other one does, then
# we should return True here
return not self.login_id and other_service.login_id
return not self.device_credentials and other_service.device_credentials

def __str__(self):
"""Return a string representation of this object."""
return super().__str__() + ', Login ID: {0}'.format(self.login_id)
return super().__str__() + ', Device Credentials: {0}'.format(
self.device_credentials)


# pylint: disable=too-few-public-methods
class MrpService(BaseService):
"""Representation of a MediaRemote Protocol service."""

def __init__(self, port):
def __init__(self, port, device_credentials=None):
"""Initialize a new MrpService."""
super().__init__(PROTOCOL_MRP, port)
self.device_credentials = device_credentials

def is_usable(self):
"""Return True if service is usable, else False."""
return True

def __str__(self):
"""Return a string representation of this object."""
return super().__str__() + ', Device Credentials: {0}'.format(
self.device_credentials)


# pylint: disable=too-few-public-methods
class AirPlayService(BaseService):
Expand Down
7 changes: 5 additions & 2 deletions pyatv/dmap/apple_tv.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,8 +340,10 @@ def start(self, initial_delay=0):

Will throw NoAsyncListenerError if no listner has been set.
"""
if self._future is not None:
if self.__listener is None:
raise exceptions.NoAsyncListenerError
elif self._future is not None:
return None

# If ensure_future, use that instead of async
if hasattr(asyncio, 'ensure_future'):
Expand Down Expand Up @@ -409,7 +411,8 @@ def __init__(self, loop, session, details, airplay):
daap_http = HttpSession(
session,
'http://{0}:{1}/'.format(details.address, self._service.port))
self._requester = DaapRequester(daap_http, self._service.login_id)
self._requester = DaapRequester(
daap_http, self._service.device_credentials)

self._apple_tv = BaseDmapAppleTV(self._requester)
self._atv_remote = DmapRemoteControl(self._apple_tv)
Expand Down
2 changes: 1 addition & 1 deletion pyatv/dmap/pairing.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def set(self, key, value, **kwargs):
@asyncio.coroutine
def get(self, key):
"""Retrieve a process specific value."""
if key == 'pairing_guid':
if key == 'credentials':
return self.pairing_guid

return None
Expand Down
Loading