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
This commit is contained in:
Alan Tse 2019-11-14 20:15:58 -08:00 committed by Martin Hjelmare
parent 806b96ef73
commit 2aad150419
10 changed files with 109 additions and 85 deletions

View File

@ -2,9 +2,8 @@
from collections import defaultdict from collections import defaultdict
import logging import logging
import voluptuous as vol
from teslajsonpy import Controller as teslaAPI, TeslaException from teslajsonpy import Controller as teslaAPI, TeslaException
import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_BATTERY_LEVEL,
@ -12,18 +11,14 @@ from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
CONF_USERNAME, CONF_USERNAME,
) )
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import aiohttp_client, config_validation as cv, discovery
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify from homeassistant.util import slugify
DOMAIN = "tesla" from .const import DOMAIN, TESLA_COMPONENTS
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
TESLA_ID_FORMAT = "{}_{}"
TESLA_ID_LIST_SCHEMA = vol.Schema([int])
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
@ -42,17 +37,8 @@ CONFIG_SCHEMA = vol.Schema(
NOTIFICATION_ID = "tesla_integration_notification" NOTIFICATION_ID = "tesla_integration_notification"
NOTIFICATION_TITLE = "Tesla integration setup" NOTIFICATION_TITLE = "Tesla integration setup"
TESLA_COMPONENTS = [
"sensor",
"lock",
"climate",
"binary_sensor",
"device_tracker",
"switch",
]
async def async_setup(hass, base_config):
def setup(hass, base_config):
"""Set up of Tesla component.""" """Set up of Tesla component."""
config = base_config.get(DOMAIN) config = base_config.get(DOMAIN)
@ -61,10 +47,15 @@ def setup(hass, base_config):
update_interval = config.get(CONF_SCAN_INTERVAL) update_interval = config.get(CONF_SCAN_INTERVAL)
if hass.data.get(DOMAIN) is None: if hass.data.get(DOMAIN) is None:
try: try:
hass.data[DOMAIN] = { websession = aiohttp_client.async_get_clientsession(hass)
"controller": teslaAPI(email, password, update_interval), controller = teslaAPI(
"devices": defaultdict(list), 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.") _LOGGER.debug("Connected to the Tesla API.")
except TeslaException as ex: except TeslaException as ex:
if ex.code == 401: if ex.code == 401:
@ -85,9 +76,7 @@ def setup(hass, base_config):
) )
_LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message)
return False return False
all_devices = controller.get_homeassistant_components()
all_devices = hass.data[DOMAIN]["controller"].list_vehicles()
if not all_devices: if not all_devices:
return False return False
@ -95,8 +84,9 @@ def setup(hass, base_config):
hass.data[DOMAIN]["devices"][device.hass_type].append(device) hass.data[DOMAIN]["devices"][device.hass_type].append(device)
for component in TESLA_COMPONENTS: 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 return True
@ -104,11 +94,12 @@ class TeslaDevice(Entity):
"""Representation of a Tesla device.""" """Representation of a Tesla device."""
def __init__(self, tesla_device, controller): def __init__(self, tesla_device, controller):
"""Initialise of the Tesla device.""" """Initialise the Tesla device."""
self.tesla_device = tesla_device self.tesla_device = tesla_device
self.controller = controller self.controller = controller
self._name = self.tesla_device.name self._name = self.tesla_device.name
self.tesla_id = slugify(self.tesla_device.uniq_name) self.tesla_id = slugify(self.tesla_device.uniq_name)
self._attributes = {}
@property @property
def name(self): def name(self):
@ -128,8 +119,19 @@ class TeslaDevice(Entity):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the device.""" """Return the state attributes of the device."""
attr = {} attr = self._attributes
if self.tesla_device.has_battery(): if self.tesla_device.has_battery():
attr[ATTR_BATTERY_LEVEL] = self.tesla_device.battery_level() attr[ATTR_BATTERY_LEVEL] = self.tesla_device.battery_level()
return attr 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()

View File

@ -8,7 +8,7 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice
_LOGGER = logging.getLogger(__name__) _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.""" """Set up the Tesla binary sensor."""
devices = [ devices = [
TeslaBinarySensor(device, hass.data[TESLA_DOMAIN]["controller"], "connectivity") TeslaBinarySensor(device, hass.data[TESLA_DOMAIN]["controller"], "connectivity")
@ -41,8 +41,8 @@ class TeslaBinarySensor(TeslaDevice, BinarySensorDevice):
"""Return the state of the binary sensor.""" """Return the state of the binary sensor."""
return self._state return self._state
def update(self): async def async_update(self):
"""Update the state of the device.""" """Update the state of the device."""
_LOGGER.debug("Updating sensor: %s", self._name) _LOGGER.debug("Updating sensor: %s", self._name)
self.tesla_device.update() await super().async_update()
self._state = self.tesla_device.get_value() self._state = self.tesla_device.get_value()

View File

@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_OFF] 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.""" """Set up the Tesla climate platform."""
devices = [ devices = [
TeslaThermostat(device, hass.data[TESLA_DOMAIN]["controller"]) TeslaThermostat(device, hass.data[TESLA_DOMAIN]["controller"])
@ -57,10 +57,10 @@ class TeslaThermostat(TeslaDevice, ClimateDevice):
""" """
return SUPPORT_HVAC return SUPPORT_HVAC
def update(self): async def async_update(self):
"""Call by the Tesla device callback to update state.""" """Call by the Tesla device callback to update state."""
_LOGGER.debug("Updating: %s", self._name) _LOGGER.debug("Updating: %s", self._name)
self.tesla_device.update() await super().async_update()
self._target_temperature = self.tesla_device.get_goal_temp() self._target_temperature = self.tesla_device.get_goal_temp()
self._temperature = self.tesla_device.get_current_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 the temperature we try to reach."""
return self._target_temperature return self._target_temperature
def set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
"""Set new target temperatures.""" """Set new target temperatures."""
_LOGGER.debug("Setting temperature for: %s", self._name) _LOGGER.debug("Setting temperature for: %s", self._name)
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
if 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.""" """Set new target hvac mode."""
_LOGGER.debug("Setting mode for: %s", self._name) _LOGGER.debug("Setting mode for: %s", self._name)
if hvac_mode == HVAC_MODE_OFF: 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: elif hvac_mode == HVAC_MODE_HEAT:
self.tesla_device.set_status(True) await self.tesla_device.set_status(True)

View File

@ -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",
}

View File

@ -1,7 +1,7 @@
"""Support for tracking Tesla cars.""" """Support for tracking Tesla cars."""
import logging 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 homeassistant.util import slugify
from . import DOMAIN as TESLA_DOMAIN from . import DOMAIN as TESLA_DOMAIN
@ -9,11 +9,13 @@ from . import DOMAIN as TESLA_DOMAIN
_LOGGER = logging.getLogger(__name__) _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.""" """Set up the Tesla tracker."""
TeslaDeviceTracker( tracker = TeslaDeviceTracker(
hass, config, see, hass.data[TESLA_DOMAIN]["devices"]["devices_tracker"] 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 return True
@ -25,14 +27,11 @@ class TeslaDeviceTracker:
self.hass = hass self.hass = hass
self.see = see self.see = see
self.devices = tesla_devices self.devices = tesla_devices
self._update_info()
track_utc_time_change(self.hass, self._update_info, second=range(0, 60, 30)) async def update_info(self, now=None):
def _update_info(self, now=None):
"""Update the device info.""" """Update the device info."""
for device in self.devices: for device in self.devices:
device.update() await device.async_update()
name = device.name name = device.name
_LOGGER.debug("Updating device position: %s", name) _LOGGER.debug("Updating device position: %s", name)
dev_id = slugify(device.uniq_name) dev_id = slugify(device.uniq_name)
@ -41,6 +40,6 @@ class TeslaDeviceTracker:
lat = location["latitude"] lat = location["latitude"]
lon = location["longitude"] lon = location["longitude"]
attrs = {"trackr_id": dev_id, "id": dev_id, "name": name} 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 dev_id=dev_id, host_name=name, gps=(lat, lon), attributes=attrs
) )

View File

@ -9,7 +9,7 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice
_LOGGER = logging.getLogger(__name__) _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.""" """Set up the Tesla lock platform."""
devices = [ devices = [
TeslaLock(device, hass.data[TESLA_DOMAIN]["controller"]) TeslaLock(device, hass.data[TESLA_DOMAIN]["controller"])
@ -26,23 +26,23 @@ class TeslaLock(TeslaDevice, LockDevice):
self._state = None self._state = None
super().__init__(tesla_device, controller) super().__init__(tesla_device, controller)
def lock(self, **kwargs): async def async_lock(self, **kwargs):
"""Send the lock command.""" """Send the lock command."""
_LOGGER.debug("Locking doors for: %s", self._name) _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.""" """Send the unlock command."""
_LOGGER.debug("Unlocking doors for: %s", self._name) _LOGGER.debug("Unlocking doors for: %s", self._name)
self.tesla_device.unlock() await self.tesla_device.unlock()
@property @property
def is_locked(self): def is_locked(self):
"""Get whether the lock is in locked state.""" """Get whether the lock is in locked state."""
return self._state == STATE_LOCKED return self._state == STATE_LOCKED
def update(self): async def async_update(self):
"""Update state of the lock.""" """Update state of the lock."""
_LOGGER.debug("Updating state for: %s", self._name) _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 self._state = STATE_LOCKED if self.tesla_device.is_locked() else STATE_UNLOCKED

View File

@ -2,7 +2,7 @@
"domain": "tesla", "domain": "tesla",
"name": "Tesla", "name": "Tesla",
"documentation": "https://www.home-assistant.io/integrations/tesla", "documentation": "https://www.home-assistant.io/integrations/tesla",
"requirements": ["teslajsonpy==0.0.26"], "requirements": ["teslajsonpy==0.2.0"],
"dependencies": [], "dependencies": [],
"codeowners": ["@zabuldon"] "codeowners": ["@zabuldon"]
} }

View File

@ -1,5 +1,4 @@
"""Support for the Tesla sensors.""" """Support for the Tesla sensors."""
from datetime import timedelta
import logging import logging
from homeassistant.const import ( from homeassistant.const import (
@ -14,10 +13,8 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=5)
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Tesla sensor platform.""" """Set up the Tesla sensor platform."""
controller = hass.data[TESLA_DOMAIN]["devices"]["controller"] controller = hass.data[TESLA_DOMAIN]["devices"]["controller"]
devices = [] devices = []
@ -26,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if device.bin_type == 0x4: if device.bin_type == 0x4:
devices.append(TeslaSensor(device, controller, "inside")) devices.append(TeslaSensor(device, controller, "inside"))
devices.append(TeslaSensor(device, controller, "outside")) devices.append(TeslaSensor(device, controller, "outside"))
else: elif device.bin_type in [0xA, 0xB, 0x5]:
devices.append(TeslaSensor(device, controller)) devices.append(TeslaSensor(device, controller))
add_entities(devices, True) add_entities(devices, True)
@ -62,10 +59,10 @@ class TeslaSensor(TeslaDevice, Entity):
"""Return the unit_of_measurement of the device.""" """Return the unit_of_measurement of the device."""
return self._unit return self._unit
def update(self): async def async_update(self):
"""Update the state from the sensor.""" """Update the state from the sensor."""
_LOGGER.debug("Updating sensor: %s", self._name) _LOGGER.debug("Updating sensor: %s", self._name)
self.tesla_device.update() await super().async_update()
units = self.tesla_device.measurement units = self.tesla_device.measurement
if self.tesla_device.bin_type == 0x4: if self.tesla_device.bin_type == 0x4:

View File

@ -9,7 +9,7 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice
_LOGGER = logging.getLogger(__name__) _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.""" """Set up the Tesla switch platform."""
controller = hass.data[TESLA_DOMAIN]["controller"] controller = hass.data[TESLA_DOMAIN]["controller"]
devices = [] devices = []
@ -30,25 +30,25 @@ class ChargerSwitch(TeslaDevice, SwitchDevice):
self._state = None self._state = None
super().__init__(tesla_device, controller) super().__init__(tesla_device, controller)
def turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Send the on command.""" """Send the on command."""
_LOGGER.debug("Enable charging: %s", self._name) _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.""" """Send the off command."""
_LOGGER.debug("Disable charging for: %s", self._name) _LOGGER.debug("Disable charging for: %s", self._name)
self.tesla_device.stop_charge() await self.tesla_device.stop_charge()
@property @property
def is_on(self): def is_on(self):
"""Get whether the switch is in on state.""" """Get whether the switch is in on state."""
return self._state == STATE_ON return self._state == STATE_ON
def update(self): async def async_update(self):
"""Update the state of the switch.""" """Update the state of the switch."""
_LOGGER.debug("Updating state for: %s", self._name) _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 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.""" """Representation of a Tesla max range charging switch."""
def __init__(self, tesla_device, controller): def __init__(self, tesla_device, controller):
"""Initialise of the switch.""" """Initialise the switch."""
self._state = None self._state = None
super().__init__(tesla_device, controller) super().__init__(tesla_device, controller)
def turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Send the on command.""" """Send the on command."""
_LOGGER.debug("Enable max range charging: %s", self._name) _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.""" """Send the off command."""
_LOGGER.debug("Disable max range charging: %s", self._name) _LOGGER.debug("Disable max range charging: %s", self._name)
self.tesla_device.set_standard() await self.tesla_device.set_standard()
@property @property
def is_on(self): def is_on(self):
"""Get whether the switch is in on state.""" """Get whether the switch is in on state."""
return self._state return self._state
def update(self): async def async_update(self):
"""Update the state of the switch.""" """Update the state of the switch."""
_LOGGER.debug("Updating state for: %s", self._name) _LOGGER.debug("Updating state for: %s", self._name)
self.tesla_device.update() await super().async_update()
self._state = bool(self.tesla_device.is_maxrange()) self._state = bool(self.tesla_device.is_maxrange())
@ -86,18 +86,19 @@ class UpdateSwitch(TeslaDevice, SwitchDevice):
"""Representation of a Tesla update switch.""" """Representation of a Tesla update switch."""
def __init__(self, tesla_device, controller): def __init__(self, tesla_device, controller):
"""Initialise of the switch.""" """Initialise the switch."""
self._state = None self._state = None
tesla_device.type = "update switch"
super().__init__(tesla_device, controller) super().__init__(tesla_device, controller)
self._name = self._name.replace("charger", "update") self._name = self._name.replace("charger", "update")
self.tesla_id = self.tesla_id.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.""" """Send the on command."""
_LOGGER.debug("Enable updates: %s %s", self._name, self.tesla_device.id()) _LOGGER.debug("Enable updates: %s %s", self._name, self.tesla_device.id())
self.controller.set_updates(self.tesla_device.id(), True) 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.""" """Send the off command."""
_LOGGER.debug("Disable updates: %s %s", self._name, self.tesla_device.id()) _LOGGER.debug("Disable updates: %s %s", self._name, self.tesla_device.id())
self.controller.set_updates(self.tesla_device.id(), False) 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.""" """Get whether the switch is in on state."""
return self._state return self._state
def update(self): async def async_update(self):
"""Update the state of the switch.""" """Update the state of the switch."""
car_id = self.tesla_device.id() car_id = self.tesla_device.id()
_LOGGER.debug("Updating state for: %s %s", self._name, car_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)) self._state = bool(self.controller.get_updates(car_id))

View File

@ -1898,7 +1898,7 @@ temperusb==1.5.3
# tensorflow==1.13.2 # tensorflow==1.13.2
# homeassistant.components.tesla # homeassistant.components.tesla
teslajsonpy==0.0.26 teslajsonpy==0.2.0
# homeassistant.components.thermoworks_smoke # homeassistant.components.thermoworks_smoke
thermoworks_smoke==0.1.8 thermoworks_smoke==0.1.8