Skip to content

Commit

Permalink
Enable config flow for Tesla (home-assistant#28744)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Update homeassistant/components/tesla/binary_sensor.py

Co-Authored-By: Paulus Schoutsen <[email protected]>

* 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 <[email protected]>
  • Loading branch information
2 people authored and MartinHjelmare committed Dec 23, 2019
1 parent edce497 commit 3aa2ae1
Show file tree
Hide file tree
Showing 17 changed files with 671 additions and 122 deletions.
9 changes: 8 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -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/*
Expand Down
2 changes: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions homeassistant/components/tesla/.translations/en.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
187 changes: 145 additions & 42 deletions homeassistant/components/tesla/__init__.py
Original file line number Diff line number Diff line change
@@ -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__)

Expand All @@ -34,69 +45,144 @@
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 = {}
Expand Down Expand Up @@ -124,6 +210,17 @@ def device_state_attributes(self):
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
Expand All @@ -134,4 +231,10 @@ async def async_will_remove_from_hass(self):

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()
30 changes: 22 additions & 8 deletions homeassistant/components/tesla/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,35 @@
_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

Expand Down
29 changes: 21 additions & 8 deletions homeassistant/components/tesla/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,34 @@
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

Expand Down
Loading

0 comments on commit 3aa2ae1

Please sign in to comment.