mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
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:
parent
edce497a0d
commit
3aa2ae1700
@ -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/*
|
||||
|
@ -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
|
||||
|
30
homeassistant/components/tesla/.translations/en.json
Normal file
30
homeassistant/components/tesla/.translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
143
homeassistant/components/tesla/config_flow.py
Normal file
143
homeassistant/components/tesla/config_flow.py
Normal 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."""
|
@ -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
|
||||
|
@ -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."""
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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})"
|
||||
|
30
homeassistant/components/tesla/strings.json
Normal file
30
homeassistant/components/tesla/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
||||
|
@ -73,6 +73,7 @@ FLOWS = [
|
||||
"sonos",
|
||||
"starline",
|
||||
"tellduslive",
|
||||
"tesla",
|
||||
"toon",
|
||||
"tplink",
|
||||
"traccar",
|
||||
|
@ -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
|
||||
|
||||
|
1
tests/components/tesla/__init__.py
Normal file
1
tests/components/tesla/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Tesla integration."""
|
160
tests/components/tesla/test_config_flow.py
Normal file
160
tests/components/tesla/test_config_flow.py
Normal 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}
|
Loading…
x
Reference in New Issue
Block a user