diff --git a/.coveragerc b/.coveragerc
index fc7a4691ef2..70d8f867e0e 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -688,7 +688,14 @@ omit =
homeassistant/components/telnet/switch.py
homeassistant/components/temper/sensor.py
homeassistant/components/tensorflow/image_processing.py
- homeassistant/components/tesla/*
+ homeassistant/components/tesla/__init__.py
+ homeassistant/components/tesla/binary_sensor.py
+ homeassistant/components/tesla/climate.py
+ homeassistant/components/tesla/const.py
+ homeassistant/components/tesla/device_tracker.py
+ homeassistant/components/tesla/lock.py
+ homeassistant/components/tesla/sensor.py
+ homeassistant/components/tesla/switch.py
homeassistant/components/tfiac/climate.py
homeassistant/components/thermoworks_smoke/sensor.py
homeassistant/components/thethingsnetwork/*
diff --git a/CODEOWNERS b/CODEOWNERS
index 4fbdca20686..a7d1d346c5f 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -322,7 +322,7 @@ homeassistant/components/tahoma/* @philklei
homeassistant/components/tautulli/* @ludeeus
homeassistant/components/tellduslive/* @fredrike
homeassistant/components/template/* @PhracturedBlue
-homeassistant/components/tesla/* @zabuldon
+homeassistant/components/tesla/* @zabuldon @alandtse
homeassistant/components/tfiac/* @fredrike @mellado
homeassistant/components/thethingsnetwork/* @fabaff
homeassistant/components/threshold/* @fabaff
diff --git a/homeassistant/components/tesla/.translations/en.json b/homeassistant/components/tesla/.translations/en.json
new file mode 100644
index 00000000000..831406a0d63
--- /dev/null
+++ b/homeassistant/components/tesla/.translations/en.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "error": {
+ "connection_error": "Error connecting; check network and retry",
+ "identifier_exists": "Email already registered",
+ "invalid_credentials": "Invalid credentials",
+ "unknown_error": "Unknown error, please report log info"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "username": "Email Address",
+ "password": "Password"
+ },
+ "description": "Please enter your information.",
+ "title": "Tesla - Configuration"
+ }
+ },
+ "title": "Tesla"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Seconds between scans"
+ }
+ }
+ }
+ }
+}
diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py
index a3d45eed01c..dbfe07271ee 100644
--- a/homeassistant/components/tesla/__init__.py
+++ b/homeassistant/components/tesla/__init__.py
@@ -1,21 +1,32 @@
"""Support for Tesla cars."""
+import asyncio
from collections import defaultdict
import logging
-from teslajsonpy import Controller as teslaAPI, TeslaException
+from teslajsonpy import Controller as TeslaAPI, TeslaException
import voluptuous as vol
+from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
+ CONF_ACCESS_TOKEN,
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
+ CONF_TOKEN,
CONF_USERNAME,
)
-from homeassistant.helpers import aiohttp_client, config_validation as cv, discovery
+from homeassistant.core import callback
+from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
-from .const import DOMAIN, TESLA_COMPONENTS
+from .config_flow import (
+ CannotConnect,
+ InvalidAuth,
+ configured_instances,
+ validate_input,
+)
+from .const import DATA_LISTENER, DOMAIN, TESLA_COMPONENTS
_LOGGER = logging.getLogger(__name__)
@@ -34,69 +45,144 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
-NOTIFICATION_ID = "tesla_integration_notification"
-NOTIFICATION_TITLE = "Tesla integration setup"
+
+@callback
+def _async_save_tokens(hass, config_entry, access_token, refresh_token):
+ hass.config_entries.async_update_entry(
+ config_entry,
+ data={
+ **config_entry.data,
+ CONF_ACCESS_TOKEN: access_token,
+ CONF_TOKEN: refresh_token,
+ },
+ )
async def async_setup(hass, base_config):
"""Set up of Tesla component."""
- config = base_config.get(DOMAIN)
- email = config.get(CONF_USERNAME)
- password = config.get(CONF_PASSWORD)
- update_interval = config.get(CONF_SCAN_INTERVAL)
- if hass.data.get(DOMAIN) is None:
+ def _update_entry(email, data=None, options=None):
+ data = data or {}
+ options = options or {CONF_SCAN_INTERVAL: 300}
+ for entry in hass.config_entries.async_entries(DOMAIN):
+ if email != entry.title:
+ continue
+ hass.config_entries.async_update_entry(entry, data=data, options=options)
+
+ config = base_config.get(DOMAIN)
+ if not config:
+ return True
+ email = config[CONF_USERNAME]
+ password = config[CONF_PASSWORD]
+ scan_interval = config[CONF_SCAN_INTERVAL]
+ if email in configured_instances(hass):
try:
- 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:
- hass.components.persistent_notification.create(
- "Error:
Please check username and password."
- "You will need to restart Home Assistant after fixing.",
- title=NOTIFICATION_TITLE,
- notification_id=NOTIFICATION_ID,
- )
- else:
- hass.components.persistent_notification.create(
- "Error:
Can't communicate with Tesla API.
"
- "Error code: {} Reason: {}"
- "You will need to restart Home Assistant after fixing."
- "".format(ex.code, ex.message),
- title=NOTIFICATION_TITLE,
- notification_id=NOTIFICATION_ID,
- )
- _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message)
+ info = await validate_input(hass, config)
+ except (CannotConnect, InvalidAuth):
return False
- all_devices = controller.get_homeassistant_components()
+ _update_entry(
+ email,
+ data={
+ CONF_ACCESS_TOKEN: info[CONF_ACCESS_TOKEN],
+ CONF_TOKEN: info[CONF_TOKEN],
+ },
+ options={CONF_SCAN_INTERVAL: scan_interval},
+ )
+ else:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data={CONF_USERNAME: email, CONF_PASSWORD: password},
+ )
+ )
+ hass.data.setdefault(DOMAIN, {})
+ hass.data[DOMAIN][email] = {CONF_SCAN_INTERVAL: scan_interval}
+ return True
+
+
+async def async_setup_entry(hass, config_entry):
+ """Set up Tesla as config entry."""
+
+ hass.data.setdefault(DOMAIN, {})
+ config = config_entry.data
+ websession = aiohttp_client.async_get_clientsession(hass)
+ email = config_entry.title
+ if email in hass.data[DOMAIN] and CONF_SCAN_INTERVAL in hass.data[DOMAIN][email]:
+ scan_interval = hass.data[DOMAIN][email][CONF_SCAN_INTERVAL]
+ hass.config_entries.async_update_entry(
+ config_entry, options={CONF_SCAN_INTERVAL: scan_interval}
+ )
+ hass.data[DOMAIN].pop(email)
+ try:
+ controller = TeslaAPI(
+ websession,
+ refresh_token=config[CONF_TOKEN],
+ update_interval=config_entry.options.get(CONF_SCAN_INTERVAL, 300),
+ )
+ (refresh_token, access_token) = await controller.connect()
+ except TeslaException as ex:
+ _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message)
+ return False
+ _async_save_tokens(hass, config_entry, access_token, refresh_token)
+ entry_data = hass.data[DOMAIN][config_entry.entry_id] = {
+ "controller": controller,
+ "devices": defaultdict(list),
+ DATA_LISTENER: [config_entry.add_update_listener(update_listener)],
+ }
+ _LOGGER.debug("Connected to the Tesla API.")
+ all_devices = entry_data["controller"].get_homeassistant_components()
+
if not all_devices:
return False
for device in all_devices:
- hass.data[DOMAIN]["devices"][device.hass_type].append(device)
+ entry_data["devices"][device.hass_type].append(device)
for component in TESLA_COMPONENTS:
+ _LOGGER.debug("Loading %s", component)
hass.async_create_task(
- discovery.async_load_platform(hass, component, DOMAIN, {}, base_config)
+ hass.config_entries.async_forward_entry_setup(config_entry, component)
)
return True
+async def async_unload_entry(hass, config_entry) -> bool:
+ """Unload a config entry."""
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(config_entry, component)
+ for component in TESLA_COMPONENTS
+ ]
+ )
+ for listener in hass.data[DOMAIN][config_entry.entry_id][DATA_LISTENER]:
+ listener()
+ username = config_entry.title
+ hass.data[DOMAIN].pop(config_entry.entry_id)
+ _LOGGER.debug("Unloaded entry for %s", username)
+ return True
+
+
+async def update_listener(hass, config_entry):
+ """Update when config_entry options update."""
+ controller = hass.data[DOMAIN][config_entry.entry_id]["controller"]
+ old_update_interval = controller.update_interval
+ controller.update_interval = config_entry.options.get(CONF_SCAN_INTERVAL)
+ _LOGGER.debug(
+ "Changing scan_interval from %s to %s",
+ old_update_interval,
+ controller.update_interval,
+ )
+
+
class TeslaDevice(Entity):
"""Representation of a Tesla device."""
- def __init__(self, tesla_device, controller):
+ def __init__(self, tesla_device, controller, config_entry):
"""Initialise the Tesla device."""
self.tesla_device = tesla_device
self.controller = controller
+ self.config_entry = config_entry
self._name = self.tesla_device.name
self.tesla_id = slugify(self.tesla_device.uniq_name)
self._attributes = {}
@@ -124,6 +210,17 @@ class TeslaDevice(Entity):
attr[ATTR_BATTERY_LEVEL] = self.tesla_device.battery_level()
return attr
+ @property
+ def device_info(self):
+ """Return the device_info of the device."""
+ return {
+ "identifiers": {(DOMAIN, self.tesla_device.id())},
+ "name": self.tesla_device.car_name(),
+ "manufacturer": "Tesla",
+ "model": self.tesla_device.car_type,
+ "sw_version": self.tesla_device.car_version,
+ }
+
async def async_added_to_hass(self):
"""Register state update callback."""
pass
@@ -134,4 +231,10 @@ class TeslaDevice(Entity):
async def async_update(self):
"""Update the state of the device."""
+ if self.controller.is_token_refreshed():
+ (refresh_token, access_token) = self.controller.get_tokens()
+ _async_save_tokens(
+ self.hass, self.config_entry, access_token, refresh_token
+ )
+ _LOGGER.debug("Saving new tokens in config_entry")
await self.tesla_device.async_update()
diff --git a/homeassistant/components/tesla/binary_sensor.py b/homeassistant/components/tesla/binary_sensor.py
index 738533a9b56..8f610d960b3 100644
--- a/homeassistant/components/tesla/binary_sensor.py
+++ b/homeassistant/components/tesla/binary_sensor.py
@@ -8,21 +8,35 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice
_LOGGER = logging.getLogger(__name__)
-async def async_setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Tesla binary sensor."""
- devices = [
- TeslaBinarySensor(device, hass.data[TESLA_DOMAIN]["controller"], "connectivity")
- for device in hass.data[TESLA_DOMAIN]["devices"]["binary_sensor"]
- ]
- add_entities(devices, True)
+ pass
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Tesla binary_sensors by config_entry."""
+ async_add_entities(
+ [
+ TeslaBinarySensor(
+ device,
+ hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"],
+ "connectivity",
+ config_entry,
+ )
+ for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][
+ "binary_sensor"
+ ]
+ ],
+ True,
+ )
class TeslaBinarySensor(TeslaDevice, BinarySensorDevice):
"""Implement an Tesla binary sensor for parking and charger."""
- def __init__(self, tesla_device, controller, sensor_type):
+ def __init__(self, tesla_device, controller, sensor_type, config_entry):
"""Initialise of a Tesla binary sensor."""
- super().__init__(tesla_device, controller)
+ super().__init__(tesla_device, controller, config_entry)
self._state = False
self._sensor_type = sensor_type
diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py
index 85fd8a8e258..d3c87035c9c 100644
--- a/homeassistant/components/tesla/climate.py
+++ b/homeassistant/components/tesla/climate.py
@@ -16,21 +16,34 @@ _LOGGER = logging.getLogger(__name__)
SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_OFF]
-async def async_setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Tesla climate platform."""
- devices = [
- TeslaThermostat(device, hass.data[TESLA_DOMAIN]["controller"])
- for device in hass.data[TESLA_DOMAIN]["devices"]["climate"]
- ]
- add_entities(devices, True)
+ pass
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Tesla binary_sensors by config_entry."""
+ async_add_entities(
+ [
+ TeslaThermostat(
+ device,
+ hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"],
+ config_entry,
+ )
+ for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][
+ "climate"
+ ]
+ ],
+ True,
+ )
class TeslaThermostat(TeslaDevice, ClimateDevice):
"""Representation of a Tesla climate."""
- def __init__(self, tesla_device, controller):
+ def __init__(self, tesla_device, controller, config_entry):
"""Initialize the Tesla device."""
- super().__init__(tesla_device, controller)
+ super().__init__(tesla_device, controller, config_entry)
self._target_temperature = None
self._temperature = None
diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py
new file mode 100644
index 00000000000..2d2bc0158d2
--- /dev/null
+++ b/homeassistant/components/tesla/config_flow.py
@@ -0,0 +1,143 @@
+"""Tesla Config Flow."""
+import logging
+
+from teslajsonpy import Controller as TeslaAPI, TeslaException
+import voluptuous as vol
+
+from homeassistant import config_entries, core, exceptions
+from homeassistant.const import (
+ CONF_ACCESS_TOKEN,
+ CONF_PASSWORD,
+ CONF_SCAN_INTERVAL,
+ CONF_TOKEN,
+ CONF_USERNAME,
+)
+from homeassistant.core import callback
+from homeassistant.helpers import aiohttp_client, config_validation as cv
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+DATA_SCHEMA = vol.Schema(
+ {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
+)
+
+
+@callback
+def configured_instances(hass):
+ """Return a set of configured Tesla instances."""
+ return set(entry.title for entry in hass.config_entries.async_entries(DOMAIN))
+
+
+class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Tesla."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ async def async_step_import(self, import_config):
+ """Import a config entry from configuration.yaml."""
+ return await self.async_step_user(import_config)
+
+ async def async_step_user(self, user_input=None):
+ """Handle the start of the config flow."""
+
+ if not user_input:
+ return self.async_show_form(
+ step_id="user",
+ data_schema=DATA_SCHEMA,
+ errors={},
+ description_placeholders={},
+ )
+
+ if user_input[CONF_USERNAME] in configured_instances(self.hass):
+ return self.async_show_form(
+ step_id="user",
+ data_schema=DATA_SCHEMA,
+ errors={CONF_USERNAME: "identifier_exists"},
+ description_placeholders={},
+ )
+
+ try:
+ info = await validate_input(self.hass, user_input)
+ except CannotConnect:
+ return self.async_show_form(
+ step_id="user",
+ data_schema=DATA_SCHEMA,
+ errors={"base": "connection_error"},
+ description_placeholders={},
+ )
+ except InvalidAuth:
+ return self.async_show_form(
+ step_id="user",
+ data_schema=DATA_SCHEMA,
+ errors={"base": "invalid_credentials"},
+ description_placeholders={},
+ )
+ return self.async_create_entry(title=user_input[CONF_USERNAME], data=info)
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return OptionsFlowHandler(config_entry)
+
+
+class OptionsFlowHandler(config_entries.OptionsFlow):
+ """Handle a option flow for Tesla."""
+
+ def __init__(self, config_entry: config_entries.ConfigEntry):
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Handle options flow."""
+ if user_input is not None:
+ return self.async_create_entry(title="", data=user_input)
+
+ data_schema = vol.Schema(
+ {
+ vol.Optional(
+ CONF_SCAN_INTERVAL,
+ default=self.config_entry.options.get(CONF_SCAN_INTERVAL, 300),
+ ): vol.All(cv.positive_int, vol.Clamp(min=300))
+ }
+ )
+ return self.async_show_form(step_id="init", data_schema=data_schema)
+
+
+async def validate_input(hass: core.HomeAssistant, data):
+ """Validate the user input allows us to connect.
+
+ Data has the keys from DATA_SCHEMA with values provided by the user.
+ """
+
+ config = {}
+ websession = aiohttp_client.async_get_clientsession(hass)
+ try:
+ controller = TeslaAPI(
+ websession,
+ email=data[CONF_USERNAME],
+ password=data[CONF_PASSWORD],
+ update_interval=300,
+ )
+ (config[CONF_TOKEN], config[CONF_ACCESS_TOKEN]) = await controller.connect(
+ test_login=True
+ )
+ except TeslaException as ex:
+ if ex.code == 401:
+ _LOGGER.error("Invalid credentials: %s", ex)
+ raise InvalidAuth()
+ _LOGGER.error("Unable to communicate with Tesla API: %s", ex)
+ raise CannotConnect()
+ _LOGGER.debug("Credentials successfully connected to the Tesla API")
+ return config
+
+
+class CannotConnect(exceptions.HomeAssistantError):
+ """Error to indicate we cannot connect."""
+
+
+class InvalidAuth(exceptions.HomeAssistantError):
+ """Error to indicate there is invalid auth."""
diff --git a/homeassistant/components/tesla/device_tracker.py b/homeassistant/components/tesla/device_tracker.py
index c205cc587eb..08e5d58ba6e 100644
--- a/homeassistant/components/tesla/device_tracker.py
+++ b/homeassistant/components/tesla/device_tracker.py
@@ -1,45 +1,70 @@
"""Support for tracking Tesla cars."""
import logging
-from homeassistant.helpers.event import async_track_utc_time_change
-from homeassistant.util import slugify
+from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
+from homeassistant.components.device_tracker.config_entry import TrackerEntity
-from . import DOMAIN as TESLA_DOMAIN
+from . import DOMAIN as TESLA_DOMAIN, TeslaDevice
_LOGGER = logging.getLogger(__name__)
-async def async_setup_scanner(hass, config, async_see, discovery_info=None):
- """Set up the Tesla 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
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Tesla binary_sensors by config_entry."""
+ entities = [
+ TeslaDeviceEntity(
+ device,
+ hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"],
+ config_entry,
+ )
+ for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][
+ "devices_tracker"
+ ]
+ ]
+ async_add_entities(entities, True)
-class TeslaDeviceTracker:
+class TeslaDeviceEntity(TeslaDevice, TrackerEntity):
"""A class representing a Tesla device."""
- def __init__(self, hass, config, see, tesla_devices):
+ def __init__(self, tesla_device, controller, config_entry):
"""Initialize the Tesla device scanner."""
- self.hass = hass
- self.see = see
- self.devices = tesla_devices
+ super().__init__(tesla_device, controller, config_entry)
+ self._latitude = None
+ self._longitude = None
+ self._attributes = {"trackr_id": self.unique_id}
+ self._listener = None
- async def update_info(self, now=None):
+ async def async_update(self):
"""Update the device info."""
- for device in self.devices:
- await device.async_update()
- name = device.name
- _LOGGER.debug("Updating device position: %s", name)
- dev_id = slugify(device.uniq_name)
- location = device.get_location()
- if location:
- lat = location["latitude"]
- lon = location["longitude"]
- attrs = {"trackr_id": dev_id, "id": dev_id, "name": name}
- await self.see(
- dev_id=dev_id, host_name=name, gps=(lat, lon), attributes=attrs
- )
+ _LOGGER.debug("Updating device position: %s", self.name)
+ await super().async_update()
+ location = self.tesla_device.get_location()
+ if location:
+ self._latitude = location["latitude"]
+ self._longitude = location["longitude"]
+ self._attributes = {
+ "trackr_id": self.unique_id,
+ "heading": location["heading"],
+ "speed": location["speed"],
+ }
+
+ @property
+ def latitude(self) -> float:
+ """Return latitude value of the device."""
+ return self._latitude
+
+ @property
+ def longitude(self) -> float:
+ """Return longitude value of the device."""
+ return self._longitude
+
+ @property
+ def should_poll(self):
+ """Return whether polling is needed."""
+ return True
+
+ @property
+ def source_type(self):
+ """Return the source type, eg gps or router, of the device."""
+ return SOURCE_TYPE_GPS
diff --git a/homeassistant/components/tesla/lock.py b/homeassistant/components/tesla/lock.py
index 5e97602357d..33eed8cf7c1 100644
--- a/homeassistant/components/tesla/lock.py
+++ b/homeassistant/components/tesla/lock.py
@@ -9,22 +9,31 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice
_LOGGER = logging.getLogger(__name__)
-async def async_setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Tesla lock platform."""
- devices = [
- TeslaLock(device, hass.data[TESLA_DOMAIN]["controller"])
- for device in hass.data[TESLA_DOMAIN]["devices"]["lock"]
+ pass
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Tesla binary_sensors by config_entry."""
+ entities = [
+ TeslaLock(
+ device,
+ hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"],
+ config_entry,
+ )
+ for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["lock"]
]
- add_entities(devices, True)
+ async_add_entities(entities, True)
class TeslaLock(TeslaDevice, LockDevice):
"""Representation of a Tesla door lock."""
- def __init__(self, tesla_device, controller):
+ def __init__(self, tesla_device, controller, config_entry):
"""Initialise of the lock."""
self._state = None
- super().__init__(tesla_device, controller)
+ super().__init__(tesla_device, controller, config_entry)
async def async_lock(self, **kwargs):
"""Send the lock command."""
diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json
index a2021092413..4a869ab0a41 100644
--- a/homeassistant/components/tesla/manifest.json
+++ b/homeassistant/components/tesla/manifest.json
@@ -1,8 +1,9 @@
{
"domain": "tesla",
"name": "Tesla",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tesla",
"requirements": ["teslajsonpy==0.2.0"],
"dependencies": [],
- "codeowners": ["@zabuldon"]
+ "codeowners": ["@zabuldon", "@alandtse"]
}
diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py
index 1cce37f232a..d93d3fa45d6 100644
--- a/homeassistant/components/tesla/sensor.py
+++ b/homeassistant/components/tesla/sensor.py
@@ -14,30 +14,34 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice
_LOGGER = logging.getLogger(__name__)
-async def async_setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Tesla sensor platform."""
- controller = hass.data[TESLA_DOMAIN]["devices"]["controller"]
- devices = []
+ pass
- for device in hass.data[TESLA_DOMAIN]["devices"]["sensor"]:
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Tesla binary_sensors by config_entry."""
+ controller = hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"]
+ entities = []
+ for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["sensor"]:
if device.bin_type == 0x4:
- devices.append(TeslaSensor(device, controller, "inside"))
- devices.append(TeslaSensor(device, controller, "outside"))
+ entities.append(TeslaSensor(device, controller, config_entry, "inside"))
+ entities.append(TeslaSensor(device, controller, config_entry, "outside"))
elif device.bin_type in [0xA, 0xB, 0x5]:
- devices.append(TeslaSensor(device, controller))
- add_entities(devices, True)
+ entities.append(TeslaSensor(device, controller, config_entry))
+ async_add_entities(entities, True)
class TeslaSensor(TeslaDevice, Entity):
"""Representation of Tesla sensors."""
- def __init__(self, tesla_device, controller, sensor_type=None):
+ def __init__(self, tesla_device, controller, config_entry, sensor_type=None):
"""Initialize of the sensor."""
self.current_value = None
self._unit = None
self.last_changed_time = None
self.type = sensor_type
- super().__init__(tesla_device, controller)
+ super().__init__(tesla_device, controller, config_entry)
if self.type:
self._name = f"{self.tesla_device.name} ({self.type})"
diff --git a/homeassistant/components/tesla/strings.json b/homeassistant/components/tesla/strings.json
new file mode 100644
index 00000000000..831406a0d63
--- /dev/null
+++ b/homeassistant/components/tesla/strings.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "error": {
+ "connection_error": "Error connecting; check network and retry",
+ "identifier_exists": "Email already registered",
+ "invalid_credentials": "Invalid credentials",
+ "unknown_error": "Unknown error, please report log info"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "username": "Email Address",
+ "password": "Password"
+ },
+ "description": "Please enter your information.",
+ "title": "Tesla - Configuration"
+ }
+ },
+ "title": "Tesla"
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Seconds between scans"
+ }
+ }
+ }
+ }
+}
diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py
index 5f432875aeb..3fc424e390d 100644
--- a/homeassistant/components/tesla/switch.py
+++ b/homeassistant/components/tesla/switch.py
@@ -9,26 +9,31 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice
_LOGGER = logging.getLogger(__name__)
-async def async_setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Tesla switch platform."""
- controller = hass.data[TESLA_DOMAIN]["controller"]
- devices = []
- for device in hass.data[TESLA_DOMAIN]["devices"]["switch"]:
+ pass
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Tesla binary_sensors by config_entry."""
+ controller = hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"]
+ entities = []
+ for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["switch"]:
if device.bin_type == 0x8:
- devices.append(ChargerSwitch(device, controller))
- devices.append(UpdateSwitch(device, controller))
+ entities.append(ChargerSwitch(device, controller, config_entry))
+ entities.append(UpdateSwitch(device, controller, config_entry))
elif device.bin_type == 0x9:
- devices.append(RangeSwitch(device, controller))
- add_entities(devices, True)
+ entities.append(RangeSwitch(device, controller, config_entry))
+ async_add_entities(entities, True)
class ChargerSwitch(TeslaDevice, SwitchDevice):
"""Representation of a Tesla charger switch."""
- def __init__(self, tesla_device, controller):
+ def __init__(self, tesla_device, controller, config_entry):
"""Initialise of the switch."""
self._state = None
- super().__init__(tesla_device, controller)
+ super().__init__(tesla_device, controller, config_entry)
async def async_turn_on(self, **kwargs):
"""Send the on command."""
@@ -55,10 +60,10 @@ class ChargerSwitch(TeslaDevice, SwitchDevice):
class RangeSwitch(TeslaDevice, SwitchDevice):
"""Representation of a Tesla max range charging switch."""
- def __init__(self, tesla_device, controller):
+ def __init__(self, tesla_device, controller, config_entry):
"""Initialise the switch."""
self._state = None
- super().__init__(tesla_device, controller)
+ super().__init__(tesla_device, controller, config_entry)
async def async_turn_on(self, **kwargs):
"""Send the on command."""
@@ -85,11 +90,11 @@ class RangeSwitch(TeslaDevice, SwitchDevice):
class UpdateSwitch(TeslaDevice, SwitchDevice):
"""Representation of a Tesla update switch."""
- def __init__(self, tesla_device, controller):
+ def __init__(self, tesla_device, controller, config_entry):
"""Initialise the switch."""
self._state = None
tesla_device.type = "update switch"
- super().__init__(tesla_device, controller)
+ super().__init__(tesla_device, controller, config_entry)
self._name = self._name.replace("charger", "update")
self.tesla_id = self.tesla_id.replace("charger", "update")
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 2b3940000e7..88ff92a57b0 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -73,6 +73,7 @@ FLOWS = [
"sonos",
"starline",
"tellduslive",
+ "tesla",
"toon",
"tplink",
"traccar",
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index e5361e7464e..2b7101dfa25 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -598,6 +598,9 @@ sunwatcher==0.2.1
# homeassistant.components.tellduslive
tellduslive==0.10.10
+# homeassistant.components.tesla
+teslajsonpy==0.2.0
+
# homeassistant.components.toon
toonapilib==3.2.4
diff --git a/tests/components/tesla/__init__.py b/tests/components/tesla/__init__.py
new file mode 100644
index 00000000000..89b1e1c0c54
--- /dev/null
+++ b/tests/components/tesla/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Tesla integration."""
diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py
new file mode 100644
index 00000000000..b6eeff54a50
--- /dev/null
+++ b/tests/components/tesla/test_config_flow.py
@@ -0,0 +1,160 @@
+"""Test the Tesla config flow."""
+from unittest.mock import patch
+
+from teslajsonpy import TeslaException
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.tesla.const import DOMAIN
+from homeassistant.const import (
+ CONF_ACCESS_TOKEN,
+ CONF_PASSWORD,
+ CONF_SCAN_INTERVAL,
+ CONF_TOKEN,
+ CONF_USERNAME,
+)
+
+from tests.common import MockConfigEntry, mock_coro
+
+
+async def test_form(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.tesla.config_flow.TeslaAPI.connect",
+ return_value=mock_coro(("test-refresh-token", "test-access-token")),
+ ), patch(
+ "homeassistant.components.tesla.async_setup", return_value=mock_coro(True)
+ ) as mock_setup, patch(
+ "homeassistant.components.tesla.async_setup_entry", return_value=mock_coro(True)
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {CONF_PASSWORD: "test", CONF_USERNAME: "test@email.com"}
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result2["title"] == "test@email.com"
+ assert result2["data"] == {
+ "token": "test-refresh-token",
+ "access_token": "test-access-token",
+ }
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_invalid_auth(hass):
+ """Test we handle invalid auth."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.tesla.config_flow.TeslaAPI.connect",
+ side_effect=TeslaException(401),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"},
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "invalid_credentials"}
+
+
+async def test_form_cannot_connect(hass):
+ """Test we handle cannot connect error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.tesla.config_flow.TeslaAPI.connect",
+ side_effect=TeslaException(code=404),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_PASSWORD: "test-password", CONF_USERNAME: "test-username"},
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "connection_error"}
+
+
+async def test_form_repeat_identifier(hass):
+ """Test we handle repeat identifiers."""
+ entry = MockConfigEntry(domain=DOMAIN, title="test-username", data={}, options=None)
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ with patch(
+ "homeassistant.components.tesla.config_flow.TeslaAPI.connect",
+ return_value=mock_coro(("test-refresh-token", "test-access-token")),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"},
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {CONF_USERNAME: "identifier_exists"}
+
+
+async def test_import(hass):
+ """Test import step."""
+
+ with patch(
+ "homeassistant.components.tesla.config_flow.TeslaAPI.connect",
+ return_value=mock_coro(("test-refresh-token", "test-access-token")),
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={CONF_PASSWORD: "test-password", CONF_USERNAME: "test-username"},
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "test-username"
+ assert result["data"][CONF_ACCESS_TOKEN] == "test-access-token"
+ assert result["data"][CONF_TOKEN] == "test-refresh-token"
+ assert result["description_placeholders"] is None
+
+
+async def test_option_flow(hass):
+ """Test config flow options."""
+ entry = MockConfigEntry(domain=DOMAIN, data={}, options=None)
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.flow.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.flow.async_configure(
+ result["flow_id"], user_input={CONF_SCAN_INTERVAL: 350}
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"] == {CONF_SCAN_INTERVAL: 350}
+
+
+async def test_option_flow_input_floor(hass):
+ """Test config flow options."""
+ entry = MockConfigEntry(domain=DOMAIN, data={}, options=None)
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.flow.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.flow.async_configure(
+ result["flow_id"], user_input={CONF_SCAN_INTERVAL: 1}
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"] == {CONF_SCAN_INTERVAL: 300}