From 2aad1504195820ad5998674b14661abab078ea04 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 14 Nov 2019 20:15:58 -0800 Subject: [PATCH] Convert Tesla to Async (#28748) * build: bump teslajsonpy to 0.2.0 * feat: add async * perf: convert unnecessary async calls to sync * fix: force real login * Revert change to HVAC_MODE_HEAT * Remove charging rate sensor * Remove tests * Remove tests * Address requested changes * Add missing sensors * Move update to async_setup_scanner * Align wtih prior update behavior --- homeassistant/components/tesla/__init__.py | 62 ++++++++++--------- .../components/tesla/binary_sensor.py | 6 +- homeassistant/components/tesla/climate.py | 16 ++--- homeassistant/components/tesla/const.py | 24 +++++++ .../components/tesla/device_tracker.py | 19 +++--- homeassistant/components/tesla/lock.py | 14 ++--- homeassistant/components/tesla/manifest.json | 2 +- homeassistant/components/tesla/sensor.py | 11 ++-- homeassistant/components/tesla/switch.py | 38 ++++++------ requirements_all.txt | 2 +- 10 files changed, 109 insertions(+), 85 deletions(-) create mode 100644 homeassistant/components/tesla/const.py diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index a08112d66b3..a3d45eed01c 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -2,9 +2,8 @@ from collections import defaultdict import logging -import voluptuous as vol from teslajsonpy import Controller as teslaAPI, TeslaException - +import voluptuous as vol from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -12,18 +11,14 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_USERNAME, ) -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import aiohttp_client, config_validation as cv, discovery from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -DOMAIN = "tesla" +from .const import DOMAIN, TESLA_COMPONENTS _LOGGER = logging.getLogger(__name__) -TESLA_ID_FORMAT = "{}_{}" -TESLA_ID_LIST_SCHEMA = vol.Schema([int]) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -42,17 +37,8 @@ CONFIG_SCHEMA = vol.Schema( NOTIFICATION_ID = "tesla_integration_notification" NOTIFICATION_TITLE = "Tesla integration setup" -TESLA_COMPONENTS = [ - "sensor", - "lock", - "climate", - "binary_sensor", - "device_tracker", - "switch", -] - -def setup(hass, base_config): +async def async_setup(hass, base_config): """Set up of Tesla component.""" config = base_config.get(DOMAIN) @@ -61,10 +47,15 @@ def setup(hass, base_config): update_interval = config.get(CONF_SCAN_INTERVAL) if hass.data.get(DOMAIN) is None: try: - hass.data[DOMAIN] = { - "controller": teslaAPI(email, password, update_interval), - "devices": defaultdict(list), - } + websession = aiohttp_client.async_get_clientsession(hass) + controller = teslaAPI( + websession, + email=email, + password=password, + update_interval=update_interval, + ) + await controller.connect(test_login=False) + hass.data[DOMAIN] = {"controller": controller, "devices": defaultdict(list)} _LOGGER.debug("Connected to the Tesla API.") except TeslaException as ex: if ex.code == 401: @@ -85,9 +76,7 @@ def setup(hass, base_config): ) _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) return False - - all_devices = hass.data[DOMAIN]["controller"].list_vehicles() - + all_devices = controller.get_homeassistant_components() if not all_devices: return False @@ -95,8 +84,9 @@ def setup(hass, base_config): hass.data[DOMAIN]["devices"][device.hass_type].append(device) for component in TESLA_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, base_config) - + hass.async_create_task( + discovery.async_load_platform(hass, component, DOMAIN, {}, base_config) + ) return True @@ -104,11 +94,12 @@ class TeslaDevice(Entity): """Representation of a Tesla device.""" def __init__(self, tesla_device, controller): - """Initialise of the Tesla device.""" + """Initialise the Tesla device.""" self.tesla_device = tesla_device self.controller = controller self._name = self.tesla_device.name self.tesla_id = slugify(self.tesla_device.uniq_name) + self._attributes = {} @property def name(self): @@ -128,8 +119,19 @@ class TeslaDevice(Entity): @property def device_state_attributes(self): """Return the state attributes of the device.""" - attr = {} - + attr = self._attributes if self.tesla_device.has_battery(): attr[ATTR_BATTERY_LEVEL] = self.tesla_device.battery_level() return attr + + async def async_added_to_hass(self): + """Register state update callback.""" + pass + + async def async_will_remove_from_hass(self): + """Prepare for unload.""" + pass + + async def async_update(self): + """Update the state of the device.""" + await self.tesla_device.async_update() diff --git a/homeassistant/components/tesla/binary_sensor.py b/homeassistant/components/tesla/binary_sensor.py index 2a452dcc832..738533a9b56 100644 --- a/homeassistant/components/tesla/binary_sensor.py +++ b/homeassistant/components/tesla/binary_sensor.py @@ -8,7 +8,7 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla binary sensor.""" devices = [ TeslaBinarySensor(device, hass.data[TESLA_DOMAIN]["controller"], "connectivity") @@ -41,8 +41,8 @@ class TeslaBinarySensor(TeslaDevice, BinarySensorDevice): """Return the state of the binary sensor.""" return self._state - def update(self): + async def async_update(self): """Update the state of the device.""" _LOGGER.debug("Updating sensor: %s", self._name) - self.tesla_device.update() + await super().async_update() self._state = self.tesla_device.get_value() diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py index 45858dcf985..85fd8a8e258 100644 --- a/homeassistant/components/tesla/climate.py +++ b/homeassistant/components/tesla/climate.py @@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_OFF] -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla climate platform.""" devices = [ TeslaThermostat(device, hass.data[TESLA_DOMAIN]["controller"]) @@ -57,10 +57,10 @@ class TeslaThermostat(TeslaDevice, ClimateDevice): """ return SUPPORT_HVAC - def update(self): + async def async_update(self): """Call by the Tesla device callback to update state.""" _LOGGER.debug("Updating: %s", self._name) - self.tesla_device.update() + await super().async_update() self._target_temperature = self.tesla_device.get_goal_temp() self._temperature = self.tesla_device.get_current_temp() @@ -83,17 +83,17 @@ class TeslaThermostat(TeslaDevice, ClimateDevice): """Return the temperature we try to reach.""" return self._target_temperature - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperatures.""" _LOGGER.debug("Setting temperature for: %s", self._name) temperature = kwargs.get(ATTR_TEMPERATURE) if temperature: - self.tesla_device.set_temperature(temperature) + await self.tesla_device.set_temperature(temperature) - def set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" _LOGGER.debug("Setting mode for: %s", self._name) if hvac_mode == HVAC_MODE_OFF: - self.tesla_device.set_status(False) + await self.tesla_device.set_status(False) elif hvac_mode == HVAC_MODE_HEAT: - self.tesla_device.set_status(True) + await self.tesla_device.set_status(True) diff --git a/homeassistant/components/tesla/const.py b/homeassistant/components/tesla/const.py new file mode 100644 index 00000000000..30a58b733ed --- /dev/null +++ b/homeassistant/components/tesla/const.py @@ -0,0 +1,24 @@ +"""Const file for Tesla cars.""" +DOMAIN = "tesla" +DATA_LISTENER = "listener" +TESLA_COMPONENTS = [ + "sensor", + "lock", + "climate", + "binary_sensor", + "device_tracker", + "switch", +] +SENSOR_ICONS = { + "battery sensor": "mdi:battery", + "range sensor": "mdi:gauge", + "mileage sensor": "mdi:counter", + "parking brake sensor": "mdi:car-brake-parking", + "charger sensor": "mdi:ev-station", + "charger switch": "mdi:battery-charging", + "update switch": "mdi:update", + "maxrange switch": "mdi:gauge-full", + "temperature sensor": "mdi:thermometer", + "location tracker": "mdi:crosshairs-gps", + "charging rate sensor": "mdi:speedometer", +} diff --git a/homeassistant/components/tesla/device_tracker.py b/homeassistant/components/tesla/device_tracker.py index 9db7bb3eb48..c205cc587eb 100644 --- a/homeassistant/components/tesla/device_tracker.py +++ b/homeassistant/components/tesla/device_tracker.py @@ -1,7 +1,7 @@ """Support for tracking Tesla cars.""" import logging -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.util import slugify from . import DOMAIN as TESLA_DOMAIN @@ -9,11 +9,13 @@ from . import DOMAIN as TESLA_DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_scanner(hass, config, see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up the Tesla tracker.""" - TeslaDeviceTracker( - hass, config, see, hass.data[TESLA_DOMAIN]["devices"]["devices_tracker"] + tracker = TeslaDeviceTracker( + hass, config, async_see, hass.data[TESLA_DOMAIN]["devices"]["devices_tracker"] ) + await tracker.update_info() + async_track_utc_time_change(hass, tracker.update_info, second=range(0, 60, 30)) return True @@ -25,14 +27,11 @@ class TeslaDeviceTracker: self.hass = hass self.see = see self.devices = tesla_devices - self._update_info() - track_utc_time_change(self.hass, self._update_info, second=range(0, 60, 30)) - - def _update_info(self, now=None): + async def update_info(self, now=None): """Update the device info.""" for device in self.devices: - device.update() + await device.async_update() name = device.name _LOGGER.debug("Updating device position: %s", name) dev_id = slugify(device.uniq_name) @@ -41,6 +40,6 @@ class TeslaDeviceTracker: lat = location["latitude"] lon = location["longitude"] attrs = {"trackr_id": dev_id, "id": dev_id, "name": name} - self.see( + await self.see( dev_id=dev_id, host_name=name, gps=(lat, lon), attributes=attrs ) diff --git a/homeassistant/components/tesla/lock.py b/homeassistant/components/tesla/lock.py index 389d6ee76e3..5e97602357d 100644 --- a/homeassistant/components/tesla/lock.py +++ b/homeassistant/components/tesla/lock.py @@ -9,7 +9,7 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla lock platform.""" devices = [ TeslaLock(device, hass.data[TESLA_DOMAIN]["controller"]) @@ -26,23 +26,23 @@ class TeslaLock(TeslaDevice, LockDevice): self._state = None super().__init__(tesla_device, controller) - def lock(self, **kwargs): + async def async_lock(self, **kwargs): """Send the lock command.""" _LOGGER.debug("Locking doors for: %s", self._name) - self.tesla_device.lock() + await self.tesla_device.lock() - def unlock(self, **kwargs): + async def async_unlock(self, **kwargs): """Send the unlock command.""" _LOGGER.debug("Unlocking doors for: %s", self._name) - self.tesla_device.unlock() + await self.tesla_device.unlock() @property def is_locked(self): """Get whether the lock is in locked state.""" return self._state == STATE_LOCKED - def update(self): + async def async_update(self): """Update state of the lock.""" _LOGGER.debug("Updating state for: %s", self._name) - self.tesla_device.update() + await super().async_update() self._state = STATE_LOCKED if self.tesla_device.is_locked() else STATE_UNLOCKED diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index 87d76c16f05..a2021092413 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -2,7 +2,7 @@ "domain": "tesla", "name": "Tesla", "documentation": "https://www.home-assistant.io/integrations/tesla", - "requirements": ["teslajsonpy==0.0.26"], + "requirements": ["teslajsonpy==0.2.0"], "dependencies": [], "codeowners": ["@zabuldon"] } diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index c737b2f0bba..1cce37f232a 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -1,5 +1,4 @@ """Support for the Tesla sensors.""" -from datetime import timedelta import logging from homeassistant.const import ( @@ -14,10 +13,8 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=5) - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla sensor platform.""" controller = hass.data[TESLA_DOMAIN]["devices"]["controller"] devices = [] @@ -26,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if device.bin_type == 0x4: devices.append(TeslaSensor(device, controller, "inside")) devices.append(TeslaSensor(device, controller, "outside")) - else: + elif device.bin_type in [0xA, 0xB, 0x5]: devices.append(TeslaSensor(device, controller)) add_entities(devices, True) @@ -62,10 +59,10 @@ class TeslaSensor(TeslaDevice, Entity): """Return the unit_of_measurement of the device.""" return self._unit - def update(self): + async def async_update(self): """Update the state from the sensor.""" _LOGGER.debug("Updating sensor: %s", self._name) - self.tesla_device.update() + await super().async_update() units = self.tesla_device.measurement if self.tesla_device.bin_type == 0x4: diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py index 985194f87b2..5f432875aeb 100644 --- a/homeassistant/components/tesla/switch.py +++ b/homeassistant/components/tesla/switch.py @@ -9,7 +9,7 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla switch platform.""" controller = hass.data[TESLA_DOMAIN]["controller"] devices = [] @@ -30,25 +30,25 @@ class ChargerSwitch(TeslaDevice, SwitchDevice): self._state = None super().__init__(tesla_device, controller) - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Send the on command.""" _LOGGER.debug("Enable charging: %s", self._name) - self.tesla_device.start_charge() + await self.tesla_device.start_charge() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Send the off command.""" _LOGGER.debug("Disable charging for: %s", self._name) - self.tesla_device.stop_charge() + await self.tesla_device.stop_charge() @property def is_on(self): """Get whether the switch is in on state.""" return self._state == STATE_ON - def update(self): + async def async_update(self): """Update the state of the switch.""" _LOGGER.debug("Updating state for: %s", self._name) - self.tesla_device.update() + await super().async_update() self._state = STATE_ON if self.tesla_device.is_charging() else STATE_OFF @@ -56,29 +56,29 @@ class RangeSwitch(TeslaDevice, SwitchDevice): """Representation of a Tesla max range charging switch.""" def __init__(self, tesla_device, controller): - """Initialise of the switch.""" + """Initialise the switch.""" self._state = None super().__init__(tesla_device, controller) - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Send the on command.""" _LOGGER.debug("Enable max range charging: %s", self._name) - self.tesla_device.set_max() + await self.tesla_device.set_max() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Send the off command.""" _LOGGER.debug("Disable max range charging: %s", self._name) - self.tesla_device.set_standard() + await self.tesla_device.set_standard() @property def is_on(self): """Get whether the switch is in on state.""" return self._state - def update(self): + async def async_update(self): """Update the state of the switch.""" _LOGGER.debug("Updating state for: %s", self._name) - self.tesla_device.update() + await super().async_update() self._state = bool(self.tesla_device.is_maxrange()) @@ -86,18 +86,19 @@ class UpdateSwitch(TeslaDevice, SwitchDevice): """Representation of a Tesla update switch.""" def __init__(self, tesla_device, controller): - """Initialise of the switch.""" + """Initialise the switch.""" self._state = None + tesla_device.type = "update switch" super().__init__(tesla_device, controller) self._name = self._name.replace("charger", "update") self.tesla_id = self.tesla_id.replace("charger", "update") - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Send the on command.""" _LOGGER.debug("Enable updates: %s %s", self._name, self.tesla_device.id()) self.controller.set_updates(self.tesla_device.id(), True) - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Send the off command.""" _LOGGER.debug("Disable updates: %s %s", self._name, self.tesla_device.id()) self.controller.set_updates(self.tesla_device.id(), False) @@ -107,8 +108,9 @@ class UpdateSwitch(TeslaDevice, SwitchDevice): """Get whether the switch is in on state.""" return self._state - def update(self): + async def async_update(self): """Update the state of the switch.""" car_id = self.tesla_device.id() _LOGGER.debug("Updating state for: %s %s", self._name, car_id) + await super().async_update() self._state = bool(self.controller.get_updates(car_id)) diff --git a/requirements_all.txt b/requirements_all.txt index 5922be08c01..407fc45a247 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1898,7 +1898,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.tesla -teslajsonpy==0.0.26 +teslajsonpy==0.2.0 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8