"""Config flow for Apple TV integration.""" import asyncio import logging from random import randrange import voluptuous as vol from homeassistant.core import callback from homeassistant import core, config_entries, exceptions from homeassistant.const import CONF_PIN, CONF_NAME, CONF_PROTOCOL, CONF_TYPE from .const import DOMAIN, CONF_IDENTIFIER, CONF_CREDENTIALS, CONF_START_OFF _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema({vol.Required(CONF_IDENTIFIER): str}) INPUT_PIN_SCHEMA = vol.Schema({vol.Required(CONF_PIN, default=None): int}) DEFAULT_START_OFF = False class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Apple TV.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH @staticmethod @callback def async_get_options_flow(config_entry): """Get options flow for this handler.""" return AppleTVOptionsFlow(config_entry) def __init__(self): self._atv = None self._identifier = None self._protocol = None self._pairing = None self._credentials = {} # Protocol -> credentials async def async_step_user(self, user_input=None): """Handle the initial step.""" import pyatv errors = {} description_placeholders = {'devices': ''} if user_input is not None: try: self._identifier = user_input[CONF_IDENTIFIER] return await self.async_find_device() except DeviceNotFound as ex: errors["base"] = "device_not_found" # This is a hack that will be removed in final version description_placeholders["devices"] = '

Found devices: ' + ', '.join(ex.devices) except pyatv.exceptions.NoServiceError: errors["base"] = "no_usable_service" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, description_placeholders=description_placeholders, errors=errors) async def async_step_zeroconf(self, discovery_info): """Handle device found via zeroconf.""" from pyatv import const service_type = discovery_info[CONF_TYPE] properties = discovery_info['properties'] if service_type == "_mediaremotetv._tcp.local.": self._identifier = properties['UniqueIdentifier'] name = properties["Name"] elif service_type == "_touch-able._tcp.local.": self._identifier = discovery_info['name'].split('.')[0] name = properties["CtlN"] elif service_type == "_appletv-v2._tcp.local.": self._identifier = discovery_info['name'].split('.')[0] name = "{0} (Home Sharing)".format(properties["Name"]) else: return self.async_abort(reason="unrecoverable") self.context["title_placeholders"] = {"name": name} return await self.async_find_device() async def async_find_device(self): """Scan for the selected device to discover services.""" import pyatv def _matches_device(dev): if self._identifier == dev.name.encode('ascii', 'ignore').decode(): # TODO: encoding should be done in pyatv return True if self._identifier == str(dev.address): return True for service in dev.services: if self._identifier == service.identifier: return True return False atvs = await pyatv.scan(self.hass.loop, timeout=3) matches = [atv for atv in atvs if _matches_device(atv)] if not matches: raise DeviceNotFound([atv.name.encode('ascii', 'ignore').decode() for atv in atvs]) self._atv = matches[0] self._protocol = self._atv.main_service().protocol for identifier in self._atv.all_identifiers: if self._is_already_configured(identifier): return self.async_abort(reason="already_configured") # If credentials were found, save them for service in self._atv.services: if service.credentials: self._credentials[service.protocol] = service.credentials return await self.async_step_confirm() async def async_step_confirm(self, user_input=None): """Handle user-confirmation of discovered node.""" if user_input is not None: if self._is_already_configured(self._identifier): return self.async_abort(reason="already_configured") try: return await self.async_begin_pairing() except: _LOGGER.exception("pair step failed") return self.async_show_form( step_id="confirm", description_placeholders={"name": self._atv.name}) async def async_begin_pairing(self): """Start pairing process for the next available protocol.""" from pyatv import pair, exceptions self._protocol = self._next_protocol_to_pair() # Dispose previous pairing sessions if self._pairing is not None: await self._pairing.close() self._pairing = None # Any more protocols to pair? Else bail out here if not self._protocol: return self._async_get_entry() # Initiate the pairing process abort_reason = None try: self._pairing = await pair(self._atv, self._protocol, self.hass.loop) await self._pairing.begin() except asyncio.TimeoutError: abort_reason = "timeout" _LOGGER.error("timeout") except OSError: _LOGGER.exception("bad error") return await self.async_step_service_problem() except exceptions.BackOffError: abort_reason = "backoff" _LOGGER.error("backoff") except Exception: _LOGGER.exception("Unexpected exception") abort_reason = "unrecoverable_error" if abort_reason: if self._pairing: await self._pairing.close() return self.async_abort(reason=abort_reason) # Choose step depending on if PIN is required from user or not if self._pairing.device_provides_pin: return await self.async_step_pair_with_pin() return await self.async_step_pair_no_pin() async def async_step_pair_with_pin(self, user_input=None): """Handle pairing step where a PIN is required from the user.""" import pyatv from pyatv import convert errors = {} _LOGGER.error("input pin") if user_input is not None: try: _LOGGER.error("got pin %d", user_input[CONF_PIN]) self._pairing.pin(user_input[CONF_PIN]) _LOGGER.error("before finish") await self._pairing.finish() _LOGGER.error("creds: %s", self._pairing.service.credentials) self._credentials[self._protocol] = self._pairing.service.credentials return await self.async_begin_pairing() except pyatv.exceptions.DeviceAuthenticationError: _LOGGER.exception("auth errot") errors["base"] = "auth" except Exception as ex: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" _LOGGER.error("will show form now") return self.async_show_form( step_id="pair_with_pin", data_schema=INPUT_PIN_SCHEMA, errors=errors, description_placeholders={ "protocol": convert.protocol_str(self._protocol)}) async def async_step_pair_no_pin(self, user_input=None): """Handle step where user has to enter a PIN on the device.""" from pyatv import convert if user_input is not None: if self._pairing.has_paired: await self._pairing.close() return await self.async_begin_pairing() return self.async_abort(reason="device_did_not_pair") random_pin = randrange(1000, stop=10000) self._pairing.pin(random_pin) return self.async_show_form( step_id="pair_no_pin", description_placeholders={ "protocol": convert.protocol_str(self._protocol), "pin": random_pin }) async def async_step_service_problem(self, user_input=None): """Inform user that a service will not be added.""" from pyatv import convert if user_input is not None: self._credentials[self._protocol] = None return await self.async_begin_pairing() return self.async_show_form( step_id="service_problem", description_placeholders={"protocol": convert.protocol_str(self._protocol)}) def _async_get_entry(self): return self.async_create_entry( title=self._atv.name, data={ CONF_IDENTIFIER: self._atv.identifier, CONF_PROTOCOL: self._atv.main_service().protocol, CONF_NAME: self._atv.name, CONF_CREDENTIALS: self._credentials, }, ) def _next_protocol_to_pair(self): def _needs_pairing(protocol): if self._atv.get_service(protocol) is None: return False return protocol not in self._credentials from pyatv import const protocols = [const.PROTOCOL_MRP, const.PROTOCOL_DMAP, const.PROTOCOL_AIRPLAY] for protocol in protocols: if _needs_pairing(protocol): return protocol return None def _is_already_configured(self, identifier): for ident in self._atv.all_identifiers: for entry in self._async_current_entries(): if entry.data[CONF_IDENTIFIER] == identifier: return True return False class AppleTVOptionsFlow(config_entries.OptionsFlow): """Handle Apple TV options.""" def __init__(self, config_entry): """Initialize Apple TV options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) async def async_step_init(self, user_input=None): """Manage the Apple TV options.""" return await self.async_step_device_options() async def async_step_device_options(self, user_input=None): """Manage the devices options.""" if user_input is not None: self.options[CONF_START_OFF] = user_input[CONF_START_OFF] return self.async_create_entry(title="", data=self.options) return self.async_show_form( step_id="device_options", data_schema=vol.Schema( { vol.Optional( CONF_START_OFF, default=self.config_entry.options.get( CONF_START_OFF, DEFAULT_START_OFF ), ): bool, } ), ) class DeviceNotFound(exceptions.HomeAssistantError): """Error to indicate device could not be found.""" def __init__(self, devices): """Initialize a new DeviceNotFound error.""" self.devices = devices