hass-core/homeassistant/components/powerwall/config_flow.py
J. Nick Koston a1d9d7116c
Prevent powerwall from switching addresses if its online ()
* Prevent powerwall from switching addresses if its online

If the wifi interface was discovered we would switch the
ip address in the entry to the wifi ip even if it was
connected via ethernet

* cover

* more cover
2022-11-20 10:38:30 -05:00

265 lines
9.6 KiB
Python

"""Config flow for Tesla Powerwall integration."""
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from tesla_powerwall import (
AccessDeniedError,
MissingAttributeError,
Powerwall,
PowerwallUnreachableError,
SiteInfo,
)
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.components import dhcp
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
from homeassistant.data_entry_flow import FlowResult
from homeassistant.util.network import is_ip_address
from . import async_last_update_was_successful
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
ENTRY_FAILURE_STATES = {
config_entries.ConfigEntryState.SETUP_ERROR,
config_entries.ConfigEntryState.SETUP_RETRY,
}
def _login_and_fetch_site_info(
power_wall: Powerwall, password: str
) -> tuple[SiteInfo, str]:
"""Login to the powerwall and fetch the base info."""
if password is not None:
power_wall.login(password)
return power_wall.get_site_info(), power_wall.get_gateway_din()
def _powerwall_is_reachable(ip_address: str, password: str) -> bool:
"""Check if the powerwall is reachable."""
try:
Powerwall(ip_address).login(password)
except AccessDeniedError:
return True
except PowerwallUnreachableError:
return False
return True
async def validate_input(
hass: core.HomeAssistant, data: dict[str, str]
) -> dict[str, str]:
"""Validate the user input allows us to connect.
Data has the keys from schema with values provided by the user.
"""
power_wall = Powerwall(data[CONF_IP_ADDRESS])
password = data[CONF_PASSWORD]
try:
site_info, gateway_din = await hass.async_add_executor_job(
_login_and_fetch_site_info, power_wall, password
)
except MissingAttributeError as err:
# Only log the exception without the traceback
_LOGGER.error(str(err))
raise WrongVersion from err
# Return info that you want to store in the config entry.
return {"title": site_info.site_name, "unique_id": gateway_din.upper()}
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Tesla Powerwall."""
VERSION = 1
def __init__(self) -> None:
"""Initialize the powerwall flow."""
self.ip_address: str | None = None
self.title: str | None = None
self.reauth_entry: config_entries.ConfigEntry | None = None
async def _async_powerwall_is_offline(
self, entry: config_entries.ConfigEntry
) -> bool:
"""Check if the power wall is offline.
We define offline by the config entry
is in a failure/retry state or the updates
are failing and the powerwall is unreachable
since device may be updating.
"""
ip_address = entry.data[CONF_IP_ADDRESS]
password = entry.data[CONF_PASSWORD]
return bool(
entry.state in ENTRY_FAILURE_STATES
or not async_last_update_was_successful(self.hass, entry)
) and not await self.hass.async_add_executor_job(
_powerwall_is_reachable, ip_address, password
)
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
"""Handle dhcp discovery."""
self.ip_address = discovery_info.ip
gateway_din = discovery_info.hostname.upper()
# The hostname is the gateway_din (unique_id)
await self.async_set_unique_id(gateway_din)
for entry in self._async_current_entries(include_ignore=False):
if entry.data[CONF_IP_ADDRESS] == discovery_info.ip:
if entry.unique_id is not None and is_ip_address(entry.unique_id):
if self.hass.config_entries.async_update_entry(
entry, unique_id=gateway_din
):
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)
)
return self.async_abort(reason="already_configured")
if entry.unique_id == gateway_din:
if await self._async_powerwall_is_offline(entry):
if self.hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_IP_ADDRESS: self.ip_address}
):
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)
)
return self.async_abort(reason="already_configured")
# Still need to abort for ignored entries
self._abort_if_unique_id_configured()
self.context["title_placeholders"] = {
"name": gateway_din,
"ip_address": self.ip_address,
}
errors, info = await self._async_try_connect(
{CONF_IP_ADDRESS: self.ip_address, CONF_PASSWORD: gateway_din[-5:]}
)
if errors:
if CONF_PASSWORD in errors:
# The default password is the gateway din last 5
# if it does not work, we have to ask
return await self.async_step_user()
return self.async_abort(reason="cannot_connect")
assert info is not None
self.title = info["title"]
return await self.async_step_confirm_discovery()
async def _async_try_connect(
self, user_input: dict[str, Any]
) -> tuple[dict[str, Any] | None, dict[str, str] | None]:
"""Try to connect to the powerwall."""
info = None
errors: dict[str, str] = {}
try:
info = await validate_input(self.hass, user_input)
except PowerwallUnreachableError:
errors[CONF_IP_ADDRESS] = "cannot_connect"
except WrongVersion:
errors["base"] = "wrong_version"
except AccessDeniedError:
errors[CONF_PASSWORD] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
return errors, info
async def async_step_confirm_discovery(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm a discovered powerwall."""
assert self.ip_address is not None
assert self.unique_id is not None
if user_input is not None:
assert self.title is not None
return self.async_create_entry(
title=self.title,
data={
CONF_IP_ADDRESS: self.ip_address,
CONF_PASSWORD: self.unique_id[-5:],
},
)
self._set_confirm_only()
self.context["title_placeholders"] = {
"name": self.title,
"ip_address": self.ip_address,
}
return self.async_show_form(
step_id="confirm_discovery",
description_placeholders={
"name": self.title,
"ip_address": self.ip_address,
},
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors: dict[str, str] | None = {}
if user_input is not None:
errors, info = await self._async_try_connect(user_input)
if not errors:
assert info is not None
if info["unique_id"]:
await self.async_set_unique_id(
info["unique_id"], raise_on_progress=False
)
self._abort_if_unique_id_configured(
updates={CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]}
)
self._async_abort_entries_match({CONF_IP_ADDRESS: self.ip_address})
return self.async_create_entry(title=info["title"], data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_IP_ADDRESS, default=self.ip_address): str,
vol.Optional(CONF_PASSWORD): str,
}
),
errors=errors,
)
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle reauth confirmation."""
assert self.reauth_entry is not None
errors: dict[str, str] | None = {}
if user_input is not None:
entry_data = self.reauth_entry.data
errors, _ = await self._async_try_connect(
{CONF_IP_ADDRESS: entry_data[CONF_IP_ADDRESS], **user_input}
)
if not errors:
self.hass.config_entries.async_update_entry(
self.reauth_entry, data={**entry_data, **user_input}
)
await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({vol.Optional(CONF_PASSWORD): str}),
errors=errors,
)
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle configuration by re-auth."""
self.reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_reauth_confirm()
class WrongVersion(exceptions.HomeAssistantError):
"""Error to indicate the powerwall uses a software version we cannot interact with."""