From c9b702c4c2471773be4bcb6faca1a184bdadaacb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 14 May 2020 03:24:22 -0600 Subject: [PATCH] Remove Automatic integration (#35029) --- .coveragerc | 1 - CODEOWNERS | 1 - .../components/automatic/__init__.py | 1 - .../components/automatic/device_tracker.py | 361 ------------------ .../components/automatic/manifest.json | 8 - requirements_all.txt | 3 - 6 files changed, 375 deletions(-) delete mode 100644 homeassistant/components/automatic/__init__.py delete mode 100644 homeassistant/components/automatic/device_tracker.py delete mode 100644 homeassistant/components/automatic/manifest.json diff --git a/.coveragerc b/.coveragerc index fb4e88903b4..ffbcda1b56a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -62,7 +62,6 @@ omit = homeassistant/components/aten_pe/* homeassistant/components/atome/* homeassistant/components/aurora_abb_powerone/sensor.py - homeassistant/components/automatic/* homeassistant/components/avea/light.py homeassistant/components/avion/light.py homeassistant/components/avri/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 07c99d48b86..5a2723a5d83 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -42,7 +42,6 @@ homeassistant/components/atome/* @baqs homeassistant/components/august/* @bdraco homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core -homeassistant/components/automatic/* @armills homeassistant/components/automation/* @home-assistant/core homeassistant/components/avea/* @pattyland homeassistant/components/avri/* @timvancann diff --git a/homeassistant/components/automatic/__init__.py b/homeassistant/components/automatic/__init__.py deleted file mode 100644 index 8a1cae16f1e..00000000000 --- a/homeassistant/components/automatic/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The automatic component.""" diff --git a/homeassistant/components/automatic/device_tracker.py b/homeassistant/components/automatic/device_tracker.py deleted file mode 100644 index 0f48ef6376d..00000000000 --- a/homeassistant/components/automatic/device_tracker.py +++ /dev/null @@ -1,361 +0,0 @@ -"""Support for the Automatic platform.""" -import asyncio -from datetime import timedelta -import json -import logging -import os - -import aioautomatic -from aiohttp import web -import voluptuous as vol - -from homeassistant.components.device_tracker import ( - ATTR_ATTRIBUTES, - ATTR_DEV_ID, - ATTR_GPS, - ATTR_GPS_ACCURACY, - ATTR_HOST_NAME, - ATTR_MAC, - PLATFORM_SCHEMA, -) -from homeassistant.components.http import HomeAssistantView -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_time_interval - -_LOGGER = logging.getLogger(__name__) - -ATTR_FUEL_LEVEL = "fuel_level" - -CONF_CLIENT_ID = "client_id" -CONF_CURRENT_LOCATION = "current_location" -CONF_DEVICES = "devices" -CONF_SECRET = "secret" - -DATA_CONFIGURING = "automatic_configurator_clients" -DATA_REFRESH_TOKEN = "refresh_token" -DEFAULT_SCOPE = ["location", "trip", "vehicle:events", "vehicle:profile"] -DEFAULT_TIMEOUT = 5 -EVENT_AUTOMATIC_UPDATE = "automatic_update" - -FULL_SCOPE = DEFAULT_SCOPE + ["current_location"] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_SECRET): cv.string, - vol.Optional(CONF_CURRENT_LOCATION, default=False): cv.boolean, - vol.Optional(CONF_DEVICES): vol.All(cv.ensure_list, [cv.string]), - } -) - - -def _get_refresh_token_from_file(hass, filename): - """Attempt to load session data from file.""" - path = hass.config.path(filename) - - if not os.path.isfile(path): - return None - - try: - with open(path) as data_file: - data = json.load(data_file) - if data is None: - return None - - return data.get(DATA_REFRESH_TOKEN) - except ValueError: - return None - - -def _write_refresh_token_to_file(hass, filename, refresh_token): - """Attempt to store session data to file.""" - path = hass.config.path(filename) - - os.makedirs(os.path.dirname(path), exist_ok=True) - with open(path, "w+") as data_file: - json.dump({DATA_REFRESH_TOKEN: refresh_token}, data_file) - - -@asyncio.coroutine -def async_setup_scanner(hass, config, async_see, discovery_info=None): - """Validate the configuration and return an Automatic scanner.""" - - hass.http.register_view(AutomaticAuthCallbackView()) - - scope = FULL_SCOPE if config.get(CONF_CURRENT_LOCATION) else DEFAULT_SCOPE - - client = aioautomatic.Client( - client_id=config[CONF_CLIENT_ID], - client_secret=config[CONF_SECRET], - client_session=async_get_clientsession(hass), - request_kwargs={"timeout": DEFAULT_TIMEOUT}, - ) - - filename = f".automatic/session-{config[CONF_CLIENT_ID]}.json" - refresh_token = yield from hass.async_add_job( - _get_refresh_token_from_file, hass, filename - ) - - @asyncio.coroutine - def initialize_data(session): - """Initialize the AutomaticData object from the created session.""" - hass.async_add_job( - _write_refresh_token_to_file, hass, filename, session.refresh_token - ) - data = AutomaticData(hass, client, session, config.get(CONF_DEVICES), async_see) - - # Load the initial vehicle data - vehicles = yield from session.get_vehicles() - for vehicle in vehicles: - hass.async_create_task(data.load_vehicle(vehicle)) - - # Create a task instead of adding a tracking job, since this task will - # run until the websocket connection is closed. - hass.loop.create_task(data.ws_connect()) - - if refresh_token is not None: - try: - session = yield from client.create_session_from_refresh_token(refresh_token) - yield from initialize_data(session) - return True - except aioautomatic.exceptions.AutomaticError as err: - _LOGGER.error(str(err)) - - configurator = hass.components.configurator - request_id = configurator.async_request_config( - "Automatic", - description=("Authorization required for Automatic device tracker."), - link_name="Click here to authorize Home Assistant.", - link_url=client.generate_oauth_url(scope), - entity_picture="/static/images/logo_automatic.png", - ) - - @asyncio.coroutine - def initialize_callback(code, state): - """Call after OAuth2 response is returned.""" - try: - session = yield from client.create_session_from_oauth_code(code, state) - yield from initialize_data(session) - configurator.async_request_done(request_id) - except aioautomatic.exceptions.AutomaticError as err: - _LOGGER.error(str(err)) - configurator.async_notify_errors(request_id, str(err)) - return False - - if DATA_CONFIGURING not in hass.data: - hass.data[DATA_CONFIGURING] = {} - - hass.data[DATA_CONFIGURING][client.state] = initialize_callback - return True - - -class AutomaticAuthCallbackView(HomeAssistantView): - """Handle OAuth finish callback requests.""" - - requires_auth = False - url = "/api/automatic/callback" - name = "api:automatic:callback" - - @callback - def get(self, request): # pylint: disable=no-self-use - """Finish OAuth callback request.""" - hass = request.app["hass"] - params = request.query - response = web.HTTPFound("/lovelace") - - if "state" not in params or "code" not in params: - if "error" in params: - _LOGGER.error("Error authorizing Automatic: %s", params["error"]) - return response - _LOGGER.error("Error authorizing Automatic. Invalid response returned") - return response - - if ( - DATA_CONFIGURING not in hass.data - or params["state"] not in hass.data[DATA_CONFIGURING] - ): - _LOGGER.error("Automatic configuration request not found") - return response - - code = params["code"] - state = params["state"] - initialize_callback = hass.data[DATA_CONFIGURING][state] - hass.async_create_task(initialize_callback(code, state)) - - return response - - -class AutomaticData: - """A class representing an Automatic cloud service connection.""" - - def __init__(self, hass, client, session, devices, async_see): - """Initialize the automatic device scanner.""" - self.hass = hass - self.devices = devices - self.vehicle_info = {} - self.vehicle_seen = {} - self.client = client - self.session = session - self.async_see = async_see - self.ws_reconnect_handle = None - self.ws_close_requested = False - - self.client.on_app_event( - lambda name, event: self.hass.async_create_task( - self.handle_event(name, event) - ) - ) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.ws_close()) - - @asyncio.coroutine - def handle_event(self, name, event): - """Coroutine to update state for a real time event.""" - - self.hass.bus.async_fire(EVENT_AUTOMATIC_UPDATE, event.data) - - if event.vehicle.id not in self.vehicle_info: - # If vehicle hasn't been seen yet, request the detailed - # info for this vehicle. - _LOGGER.info("New vehicle found") - try: - vehicle = yield from event.get_vehicle() - except aioautomatic.exceptions.AutomaticError as err: - _LOGGER.error(str(err)) - return - yield from self.get_vehicle_info(vehicle) - - if event.created_at < self.vehicle_seen[event.vehicle.id]: - # Skip events received out of order - _LOGGER.debug( - "Skipping out of order event. Event Created %s. Last seen event: %s", - event.created_at, - self.vehicle_seen[event.vehicle.id], - ) - return - self.vehicle_seen[event.vehicle.id] = event.created_at - - kwargs = self.vehicle_info[event.vehicle.id] - if kwargs is None: - # Ignored device - return - - # If this is a vehicle status report, update the fuel level - if name == "vehicle:status_report": - fuel_level = event.vehicle.fuel_level_percent - if fuel_level is not None: - kwargs[ATTR_ATTRIBUTES][ATTR_FUEL_LEVEL] = fuel_level - - # Send the device seen notification - if event.location is not None: - kwargs[ATTR_GPS] = (event.location.lat, event.location.lon) - kwargs[ATTR_GPS_ACCURACY] = event.location.accuracy_m - - yield from self.async_see(**kwargs) - - @asyncio.coroutine - def ws_connect(self, now=None): - """Open the websocket connection.""" - - self.ws_close_requested = False - - if self.ws_reconnect_handle is not None: - _LOGGER.debug("Retrying websocket connection") - try: - ws_loop_future = yield from self.client.ws_connect() - except aioautomatic.exceptions.UnauthorizedClientError: - _LOGGER.error( - "Client unauthorized for websocket connection. " - "Ensure Websocket is selected in the Automatic " - "developer application event delivery preferences" - ) - return - except aioautomatic.exceptions.AutomaticError as err: - if self.ws_reconnect_handle is None: - # Show log error and retry connection every 5 minutes - _LOGGER.error("Error opening websocket connection: %s", err) - self.ws_reconnect_handle = async_track_time_interval( - self.hass, self.ws_connect, timedelta(minutes=5) - ) - return - - if self.ws_reconnect_handle is not None: - self.ws_reconnect_handle() - self.ws_reconnect_handle = None - - _LOGGER.info("Websocket connected") - - try: - yield from ws_loop_future - except aioautomatic.exceptions.AutomaticError as err: - _LOGGER.error(str(err)) - - _LOGGER.info("Websocket closed") - - # If websocket was close was not requested, attempt to reconnect - if not self.ws_close_requested: - self.hass.loop.create_task(self.ws_connect()) - - @asyncio.coroutine - def ws_close(self): - """Close the websocket connection.""" - self.ws_close_requested = True - if self.ws_reconnect_handle is not None: - self.ws_reconnect_handle() - self.ws_reconnect_handle = None - - yield from self.client.ws_close() - - @asyncio.coroutine - def load_vehicle(self, vehicle): - """Load the vehicle's initial state and update hass.""" - kwargs = yield from self.get_vehicle_info(vehicle) - yield from self.async_see(**kwargs) - - @asyncio.coroutine - def get_vehicle_info(self, vehicle): - """Fetch the latest vehicle info from automatic.""" - - name = vehicle.display_name - if name is None: - name = " ".join( - filter(None, (str(vehicle.year), vehicle.make, vehicle.model)) - ) - - if self.devices is not None and name not in self.devices: - self.vehicle_info[vehicle.id] = None - return - - self.vehicle_info[vehicle.id] = kwargs = { - ATTR_DEV_ID: vehicle.id, - ATTR_HOST_NAME: name, - ATTR_MAC: vehicle.id, - ATTR_ATTRIBUTES: {ATTR_FUEL_LEVEL: vehicle.fuel_level_percent}, - } - self.vehicle_seen[vehicle.id] = vehicle.updated_at or vehicle.created_at - - if vehicle.latest_location is not None: - location = vehicle.latest_location - kwargs[ATTR_GPS] = (location.lat, location.lon) - kwargs[ATTR_GPS_ACCURACY] = location.accuracy_m - return kwargs - - trips = [] - try: - # Get the most recent trip for this vehicle - trips = yield from self.session.get_trips(vehicle=vehicle.id, limit=1) - except aioautomatic.exceptions.AutomaticError as err: - _LOGGER.error(str(err)) - - if trips: - location = trips[0].end_location - kwargs[ATTR_GPS] = (location.lat, location.lon) - kwargs[ATTR_GPS_ACCURACY] = location.accuracy_m - - if trips[0].ended_at >= self.vehicle_seen[vehicle.id]: - self.vehicle_seen[vehicle.id] = trips[0].ended_at - - return kwargs diff --git a/homeassistant/components/automatic/manifest.json b/homeassistant/components/automatic/manifest.json deleted file mode 100644 index e0d06ff0f1f..00000000000 --- a/homeassistant/components/automatic/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "automatic", - "name": "Automatic", - "documentation": "https://www.home-assistant.io/integrations/automatic", - "requirements": ["aioautomatic==0.6.5"], - "dependencies": ["configurator", "http"], - "codeowners": ["@armills"] -} diff --git a/requirements_all.txt b/requirements_all.txt index 31acf734e6c..a4b01b38b3a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -155,9 +155,6 @@ aioambient==1.1.1 # homeassistant.components.asuswrt aioasuswrt==1.2.5 -# homeassistant.components.automatic -aioautomatic==0.6.5 - # homeassistant.components.aws aiobotocore==0.11.1