Enable config flow for Tesla (#28744)

* build: bump teslajsonpy to 0.2.0

* Remove tests

* feat: add config flow

* feat: add async

* perf: convert unnecessary async calls to sync

* feat: add charger voltage and current sensor

* feat: add options flow

* build: bump teslajsonpy to 0.2.0

* Remove icon property

* Revert climate mode change

* Remove charger sensor

* Simplify async_setup_platform

* Update homeassistant/components/tesla/sensor.py

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* Update homeassistant/components/tesla/binary_sensor.py

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* Address requested changes

* Fix pylint error

* Address requested changes

* Update codeowners

* Fix pylint error

* Address requested changes

* Address requested change

* Remove unnecessary check for existing config entry

* Load scan_interval in async_setup_entry

* Include coverage of config_flow

* Add tests for full coverage

* Address requested test changes

* Remove unnecessary init lines

* Remove unnecessary init

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Alan Tse 2019-12-23 12:54:25 -08:00 committed by Martin Hjelmare
parent edce497a0d
commit 3aa2ae1700
17 changed files with 671 additions and 122 deletions

View File

@ -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/*

View File

@ -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

View File

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

View File

@ -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:<br />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:<br />Can't communicate with Tesla API.<br />"
"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()

View File

@ -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

View File

@ -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

View File

@ -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."""

View File

@ -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

View File

@ -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."""

View File

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

View File

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

View File

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

View File

@ -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")

View File

@ -73,6 +73,7 @@ FLOWS = [
"sonos",
"starline",
"tellduslive",
"tesla",
"toon",
"tplink",
"traccar",

View File

@ -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

View File

@ -0,0 +1 @@
"""Tests for the Tesla integration."""

View File

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