async def async_setup_entry(hass, entry): """Set up Subaru from a config entry.""" config = entry.data websession = aiohttp_client.async_get_clientsession(hass) try: controller = SubaruAPI( websession, config[CONF_USERNAME], config[CONF_PASSWORD], config[CONF_DEVICE_ID], config[CONF_PIN], None, config[CONF_COUNTRY], update_interval=UPDATE_INTERVAL, fetch_interval=FETCH_INTERVAL, ) _LOGGER.debug("Using subarulink %s", controller.version) await controller.connect() except InvalidCredentials: _LOGGER.error("Invalid account") return False except SubaruException as err: raise ConfigEntryNotReady(err.message) from err vehicle_info = {} for vin in controller.get_vehicles(): vehicle_info[vin] = get_vehicle_info(controller, vin) async def async_update_data(): """Fetch data from API endpoint.""" try: return await refresh_subaru_data(entry, vehicle_info, controller) except SubaruException as err: raise UpdateFailed(err.message) from err coordinator = DataUpdateCoordinator( hass, _LOGGER, name=COORDINATOR_NAME, update_method=async_update_data, update_interval=timedelta(seconds=FETCH_INTERVAL), ) await coordinator.async_refresh() hass.data[DOMAIN][entry.entry_id] = { ENTRY_CONTROLLER: controller, ENTRY_COORDINATOR: coordinator, ENTRY_VEHICLES: vehicle_info, } for component in SUPPORTED_PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) return True
async def validate_login_creds(self, data): """Validate the user input allows us to connect. data: contains values provided by the user. """ websession = aiohttp_client.async_get_clientsession(self.hass) now = datetime.now() if not data.get(CONF_DEVICE_ID): data[CONF_DEVICE_ID] = int(now.timestamp()) date = now.strftime("%Y-%m-%d") device_name = "Home Assistant: Added " + date self.controller = SubaruAPI( websession, username=data[CONF_USERNAME], password=data[CONF_PASSWORD], device_id=data[CONF_DEVICE_ID], pin=None, device_name=device_name, country=data[CONF_COUNTRY], ) _LOGGER.debug( "Setting up first time connection to Subaru API. This may take up to 20 seconds" ) if await self.controller.connect(): _LOGGER.debug("Successfully authenticated and authorized with Subaru API") self.config_data.update(data)
async def single_command(self, cmd, vin): """Initialize connection and execute as single command.""" success = False self._session = ClientSession() self._ctrl = Controller( self._session, self._config["username"], self._config["password"], self._config["device_id"], self._config["pin"], self._config["device_name"], ) if await self._connect(interactive=False, vin=vin): try: if cmd == "status": success = await self._fetch() pprint(self._car_data) elif cmd == "lock": success = await self._ctrl.lock(self._current_vin) elif cmd == "unlock": success = await self._ctrl.unlock(self._current_vin) elif cmd == "lights": success = await self._ctrl.lights(self._current_vin) elif cmd == "horn": success = await self._ctrl.horn(self._current_vin) elif cmd == "locate": success = await self._ctrl.update(self._current_vin) await self._fetch() pprint(self._car_data.get("location")) elif cmd == "remote_start": success = await self._ctrl.remote_start(self._current_vin) elif cmd == "remote_stop": success = await self._ctrl.remote_stop(self._current_vin) elif cmd == "charge": success = await self._ctrl.charge_start(self._current_vin) else: LOGGER.error("Unsupported command") except SubaruException as exc: LOGGER.error("SubaruException caught: %s", exc.message) if success: print( f"{OK}Command '{cmd}' completed for {self._current_vin}{ENDC}") sys.exit(0) else: print( f"{FAIL}Command '{cmd}' failed for {self._current_vin}{ENDC}") sys.exit(1)
def _init_controller(self): self._session = ClientSession() self._ctrl = Controller( self._session, self.config["username"], self.config["password"], self.config["device_id"], self.config["pin"], self.config["device_name"], country=self.config["country"], )
def _ctrl(self): if self.__ctrl is None: self._session = ClientSession() self._cars = [] self.__ctrl = Controller( self._session, # aiohttp os.getenv("SUBARU_USERNAME"), os.getenv("SUBARU_PASSWORD"), os.getenv("SUBARU_DEVICE_ID"), self._pin, os.getenv("SUBARU_DEVICE_NAME"), ) return self.__ctrl
async def run(self): """Initialize connection and start CLI loop.""" self._session = ClientSession() self._ctrl = Controller( self._session, self._config["username"], self._config["password"], self._config["device_id"], self._config["pin"], self._config["device_name"], ) try: if await self._connect(): await self._cli_loop() except (KeyboardInterrupt, EOFError): await self._quit(0)
class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Subaru.""" VERSION = 1 def __init__(self): """Initialize config flow.""" self.config_data = {CONF_PIN: None} self.controller = None async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" error = None if user_input: self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]}) try: await self.validate_login_creds(user_input) except InvalidCredentials: error = {"base": "invalid_auth"} except SubaruException as ex: _LOGGER.error("Unable to communicate with Subaru API: %s", ex.message) return self.async_abort(reason="cannot_connect") else: if self.controller.is_pin_required(): return await self.async_step_pin() return self.async_create_entry( title=user_input[CONF_USERNAME], data=self.config_data ) return self.async_show_form( step_id="user", data_schema=vol.Schema( { vol.Required( CONF_USERNAME, default=user_input.get(CONF_USERNAME) if user_input else "", ): str, vol.Required( CONF_PASSWORD, default=user_input.get(CONF_PASSWORD) if user_input else "", ): str, vol.Required( CONF_COUNTRY, default=user_input.get(CONF_COUNTRY) if user_input else COUNTRY_USA, ): vol.In([COUNTRY_CAN, COUNTRY_USA]), } ), errors=error, ) @staticmethod @callback def async_get_options_flow(config_entry): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) async def validate_login_creds(self, data): """Validate the user input allows us to connect. data: contains values provided by the user. """ websession = aiohttp_client.async_get_clientsession(self.hass) now = datetime.now() if not data.get(CONF_DEVICE_ID): data[CONF_DEVICE_ID] = int(now.timestamp()) date = now.strftime("%Y-%m-%d") device_name = "Home Assistant: Added " + date self.controller = SubaruAPI( websession, username=data[CONF_USERNAME], password=data[CONF_PASSWORD], device_id=data[CONF_DEVICE_ID], pin=None, device_name=device_name, country=data[CONF_COUNTRY], ) _LOGGER.debug( "Setting up first time connection to Subaru API. This may take up to 20 seconds" ) if await self.controller.connect(): _LOGGER.debug("Successfully authenticated and authorized with Subaru API") self.config_data.update(data) async def async_step_pin(self, user_input=None): """Handle second part of config flow, if required.""" error = None if user_input and self.controller.update_saved_pin(user_input[CONF_PIN]): try: vol.Match(r"[0-9]{4}")(user_input[CONF_PIN]) await self.controller.test_pin() except vol.Invalid: error = {"base": "bad_pin_format"} except InvalidPIN: error = {"base": "incorrect_pin"} else: _LOGGER.debug("PIN successfully tested") self.config_data.update(user_input) return self.async_create_entry( title=self.config_data[CONF_USERNAME], data=self.config_data ) return self.async_show_form(step_id="pin", data_schema=PIN_SCHEMA, errors=error)
class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Subaru.""" VERSION = 1 def __init__(self): """Initialize config flow.""" self.config_data = {CONF_PIN: None} self.controller = None async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" error = None if user_input: self._async_abort_entries_match( {CONF_USERNAME: user_input[CONF_USERNAME]}) try: await self.validate_login_creds(user_input) except InvalidCredentials: error = {"base": "invalid_auth"} except SubaruException as ex: _LOGGER.error("Unable to communicate with Subaru API: %s", ex.message) return self.async_abort(reason="cannot_connect") else: if not self.controller.device_registered: _LOGGER.debug("2FA validation is required") return await self.async_step_two_factor() if self.controller.is_pin_required(): return await self.async_step_pin() return self.async_create_entry(title=user_input[CONF_USERNAME], data=self.config_data) return self.async_show_form( step_id="user", data_schema=vol.Schema({ vol.Required( CONF_USERNAME, default=user_input.get(CONF_USERNAME) if user_input else "", ): str, vol.Required( CONF_PASSWORD, default=user_input.get(CONF_PASSWORD) if user_input else "", ): str, vol.Required( CONF_COUNTRY, default=user_input.get(CONF_COUNTRY) if user_input else COUNTRY_USA, ): vol.In([COUNTRY_CAN, COUNTRY_USA]), }), errors=error, ) @staticmethod @callback def async_get_options_flow( config_entry: config_entries.ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) async def validate_login_creds(self, data): """Validate the user input allows us to connect. data: contains values provided by the user. """ websession = aiohttp_client.async_get_clientsession(self.hass) now = datetime.now() if not data.get(CONF_DEVICE_ID): data[CONF_DEVICE_ID] = int(now.timestamp()) date = now.strftime("%Y-%m-%d") device_name = "Home Assistant: Added " + date self.controller = SubaruAPI( websession, username=data[CONF_USERNAME], password=data[CONF_PASSWORD], device_id=data[CONF_DEVICE_ID], pin=None, device_name=device_name, country=data[CONF_COUNTRY], ) _LOGGER.debug("Setting up first time connection to Subaru API") if await self.controller.connect(): _LOGGER.debug("Successfully authenticated with Subaru API") self.config_data.update(data) async def async_step_two_factor(self, user_input=None): """Select contact method and request 2FA code from Subaru.""" error = None if user_input: # self.controller.contact_methods is a dict: # {"phone":"555-555-5555", "userName":"******"} selected_method = next( k for k, v in self.controller.contact_methods.items() if v == user_input[CONF_CONTACT_METHOD]) if await self.controller.request_auth_code(selected_method): return await self.async_step_two_factor_validate() return self.async_abort(reason="two_factor_request_failed") data_schema = vol.Schema({ vol.Required(CONF_CONTACT_METHOD): vol.In(list(self.controller.contact_methods.values())) }) return self.async_show_form(step_id="two_factor", data_schema=data_schema, errors=error) async def async_step_two_factor_validate(self, user_input=None): """Validate received 2FA code with Subaru.""" error = None if user_input: try: vol.Match(r"^[0-9]{6}$")(user_input[CONF_VALIDATION_CODE]) if await self.controller.submit_auth_code( user_input[CONF_VALIDATION_CODE]): if self.controller.is_pin_required(): return await self.async_step_pin() return self.async_create_entry( title=self.config_data[CONF_USERNAME], data=self.config_data) error = {"base": "incorrect_validation_code"} except vol.Invalid: error = {"base": "bad_validation_code_format"} data_schema = vol.Schema({vol.Required(CONF_VALIDATION_CODE): str}) return self.async_show_form(step_id="two_factor_validate", data_schema=data_schema, errors=error) async def async_step_pin(self, user_input=None): """Handle second part of config flow, if required.""" error = None if user_input and self.controller.update_saved_pin( user_input[CONF_PIN]): try: vol.Match(r"[0-9]{4}")(user_input[CONF_PIN]) await self.controller.test_pin() except vol.Invalid: error = {"base": "bad_pin_format"} except InvalidPIN: error = {"base": "incorrect_pin"} else: _LOGGER.debug("PIN successfully tested") self.config_data.update(user_input) return self.async_create_entry( title=self.config_data[CONF_USERNAME], data=self.config_data) return self.async_show_form(step_id="pin", data_schema=PIN_SCHEMA, errors=error)
async def async_setup_entry(hass, entry): """Set up Subaru from a config entry.""" config = entry.data websession = aiohttp_client.async_get_clientsession(hass) date = datetime.now().strftime("%Y-%m-%d") device_name = "Home Assistant: Added " + date try: controller = SubaruAPI( websession, config[CONF_USERNAME], config[CONF_PASSWORD], config[CONF_DEVICE_ID], config[CONF_PIN], device_name, update_interval=entry.options.get( CONF_HARD_POLL_INTERVAL, DEFAULT_HARD_POLL_INTERVAL ), ) await controller.connect() except SubaruException as err: raise ConfigEntryNotReady(err) from err vehicle_info = {} remote_services = [] for vin in controller.get_vehicles(): vehicle_info[vin] = get_vehicle_info(controller, vin) if vehicle_info[vin][VEHICLE_HAS_SAFETY_SERVICE]: remote_services.append(REMOTE_SERVICE_FETCH) if vehicle_info[vin][VEHICLE_HAS_REMOTE_SERVICE]: remote_services.append(REMOTE_SERVICE_HORN) remote_services.append(REMOTE_SERVICE_LIGHTS) remote_services.append(REMOTE_SERVICE_LOCK) remote_services.append(REMOTE_SERVICE_UNLOCK) remote_services.append(REMOTE_SERVICE_UPDATE) if ( vehicle_info[vin][VEHICLE_HAS_REMOTE_START] or vehicle_info[vin][VEHICLE_HAS_EV] ): remote_services.append(REMOTE_SERVICE_REMOTE_START) remote_services.append(REMOTE_SERVICE_REMOTE_STOP) if vehicle_info[vin][VEHICLE_HAS_EV]: remote_services.append(REMOTE_SERVICE_CHARGE_START) async def async_update_data(): """Fetch data from API endpoint.""" try: return await subaru_update(vehicle_info, controller) except SubaruException as err: raise UpdateFailed(err) from err coordinator = DataUpdateCoordinator( hass, _LOGGER, name=COORDINATOR_NAME, update_method=async_update_data, update_interval=timedelta( seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) ), ) await coordinator.async_refresh() hass.data.get(DOMAIN)[entry.entry_id] = { ENTRY_CONTROLLER: controller, ENTRY_COORDINATOR: coordinator, ENTRY_VEHICLES: vehicle_info, ENTRY_LISTENER: entry.add_update_listener(update_listener), } for component in SUPPORTED_PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) async def async_remote_service(call): """Execute remote services.""" vin = call.data[VEHICLE_VIN].upper() success = False if vin not in vehicle_info.keys(): hass.components.persistent_notification.create( f"ERROR - Invalid VIN: {vin}", "Subaru" ) else: if call.service == REMOTE_SERVICE_FETCH: await coordinator.async_refresh() return try: _LOGGER.debug("calling %s", call.service) hass.components.persistent_notification.create( f"Calling Subaru Service: {call.service}:{vin}\nThis may take 10-15 seconds.", "Subaru", DOMAIN, ) if call.service == REMOTE_SERVICE_UPDATE: vehicle = vehicle_info[vin] success = await refresh_subaru_data( vehicle, controller, override_interval=True ) else: success = await getattr(controller, call.service)(vin) except InvalidPIN: hass.components.persistent_notification.dismiss(DOMAIN) hass.components.persistent_notification.create( "ERROR - Invalid PIN", "Subaru" ) if success and call.service in SERVICES_THAT_NEED_FETCH: await coordinator.async_refresh() hass.components.persistent_notification.dismiss(DOMAIN) if success: hass.components.persistent_notification.create( f"Command completed: {call.service}:{vin}", "Subaru" ) else: hass.components.persistent_notification.create( f"ERROR - Command failed: {call.service}:{vin}", "Subaru" ) for service in remote_services: hass.services.async_register( DOMAIN, service, async_remote_service, schema=REMOTE_SERVICE_SCHEMA ) return True
async def async_setup_entry(hass, entry): """Set up Subaru from a config entry.""" config = entry.data websession = aiohttp_client.async_create_clientsession(hass) # Backwards compatibility for configs made before v0.3.0 country = config.get(CONF_COUNTRY) if not country: country = COUNTRY_USA try: controller = SubaruAPI( websession, config[CONF_USERNAME], config[CONF_PASSWORD], config[CONF_DEVICE_ID], config[CONF_PIN], None, country=country, update_interval=UPDATE_INTERVAL, fetch_interval=FETCH_INTERVAL, ) _LOGGER.debug("Using subarulink %s", controller.version) await controller.connect() except InvalidCredentials: _LOGGER.error("Invalid account") return False except SubaruException as err: raise ConfigEntryNotReady(err) from err vehicles = {} for vin in controller.get_vehicles(): vehicles[vin] = get_vehicle_info(controller, vin) async def async_update_data(): """Fetch data from API endpoint.""" try: return await refresh_subaru_data(entry, vehicles, controller) except SubaruException as err: raise UpdateFailed(err.message) from err coordinator = DataUpdateCoordinator( hass, _LOGGER, name=COORDINATOR_NAME, update_method=async_update_data, update_interval=timedelta(seconds=FETCH_INTERVAL), ) await coordinator.async_refresh() hass.data.get(DOMAIN)[entry.entry_id] = { ENTRY_CONTROLLER: controller, ENTRY_COORDINATOR: coordinator, ENTRY_VEHICLES: vehicles, } for component in SUPPORTED_PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component)) async def async_call_service(call): """Execute subaru service.""" _LOGGER.warn( "This Subaru-specific service is deprecated and will be removed in v0.7.0. Use button or lock entities (or their respective services) to actuate remove vehicle services." ) vin = call.data[VEHICLE_VIN].upper() arg = None if vin in vehicles: await async_call_remote_service( hass, controller, call.service, vehicles[vin], arg, entry.options.get(CONF_NOTIFICATION_OPTION), ) await coordinator.async_refresh() return hass.components.persistent_notification.create( f"ERROR - Invalid VIN provided while calling {call.service}", "Subaru") raise HomeAssistantError( f"Invalid VIN provided while calling {call.service}") async def async_remote_start(call): """Start the vehicle engine.""" dev_reg = device_registry.async_get(hass) device_entry = dev_reg.async_get(call.data[ATTR_DEVICE_ID]) if device_entry: vin = list(device_entry.identifiers)[0][1] _LOGGER.info( "Remote engine start initiated with climate preset: %s", call.data[REMOTE_CLIMATE_PRESET_NAME], ) await async_call_remote_service( hass, controller, call.service, vehicles[vin], call.data[REMOTE_CLIMATE_PRESET_NAME], entry.options.get(CONF_NOTIFICATION_OPTION), ) await coordinator.async_refresh() else: raise HomeAssistantError( f"device_id {call.data[ATTR_DEVICE_ID]} not found") supported_services = get_supported_services(vehicles) for service in supported_services: if service == REMOTE_SERVICE_REMOTE_START: hass.services.async_register( DOMAIN, service, async_remote_start, schema=vol.Schema({ vol.Required(ATTR_DEVICE_ID): cv.string, vol.Required(REMOTE_CLIMATE_PRESET_NAME): cv.string, }), ) else: hass.services.async_register( DOMAIN, service, async_call_service, schema=vol.Schema({vol.Required(VEHICLE_VIN): cv.string}), ) return True