260 lines
9.5 KiB
Python
260 lines
9.5 KiB
Python
"""Config flow for elmax-cloud integration."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from elmax_api.exceptions import ElmaxBadLoginError, ElmaxBadPinError, ElmaxNetworkError
|
|
from elmax_api.http import Elmax
|
|
from elmax_api.model.panel import PanelEntry
|
|
import voluptuous as vol
|
|
|
|
from homeassistant import config_entries
|
|
from homeassistant.data_entry_flow import FlowResult
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
|
|
from .const import (
|
|
CONF_ELMAX_PANEL_ID,
|
|
CONF_ELMAX_PANEL_NAME,
|
|
CONF_ELMAX_PANEL_PIN,
|
|
CONF_ELMAX_PASSWORD,
|
|
CONF_ELMAX_USERNAME,
|
|
DOMAIN,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
LOGIN_FORM_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(CONF_ELMAX_USERNAME): str,
|
|
vol.Required(CONF_ELMAX_PASSWORD): str,
|
|
}
|
|
)
|
|
|
|
|
|
def _store_panel_by_name(
|
|
panel: PanelEntry, username: str, panel_names: dict[str, str]
|
|
) -> None:
|
|
original_panel_name = panel.get_name_by_user(username=username)
|
|
panel_id = panel.hash
|
|
collisions_count = 0
|
|
panel_name = original_panel_name
|
|
while panel_name in panel_names:
|
|
# Handle same-name collision.
|
|
collisions_count += 1
|
|
panel_name = f"{original_panel_name} ({collisions_count})"
|
|
panel_names[panel_name] = panel_id
|
|
|
|
|
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|
"""Handle a config flow for elmax-cloud."""
|
|
|
|
VERSION = 1
|
|
_client: Elmax
|
|
_username: str
|
|
_password: str
|
|
_panels_schema: vol.Schema
|
|
_panel_names: dict
|
|
_reauth_username: str | None
|
|
_reauth_panelid: str | None
|
|
|
|
async def async_step_user(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> FlowResult:
|
|
"""Handle a flow initialized by the user."""
|
|
# When invokes without parameters, show the login form.
|
|
if user_input is None:
|
|
return self.async_show_form(step_id="user", data_schema=LOGIN_FORM_SCHEMA)
|
|
|
|
username = user_input[CONF_ELMAX_USERNAME]
|
|
password = user_input[CONF_ELMAX_PASSWORD]
|
|
|
|
# Otherwise, it means we are handling now the "submission" of the user form.
|
|
# In this case, let's try to log in to the Elmax cloud and retrieve the available panels.
|
|
try:
|
|
client = await self._async_login(username=username, password=password)
|
|
|
|
except ElmaxBadLoginError:
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=LOGIN_FORM_SCHEMA,
|
|
errors={"base": "invalid_auth"},
|
|
)
|
|
except ElmaxNetworkError:
|
|
_LOGGER.exception("A network error occurred")
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=LOGIN_FORM_SCHEMA,
|
|
errors={"base": "network_error"},
|
|
)
|
|
|
|
# If the login succeeded, retrieve the list of available panels and filter the online ones
|
|
online_panels = [x for x in await client.list_control_panels() if x.online]
|
|
|
|
# If no online panel was found, we display an error in the next UI.
|
|
if not online_panels:
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=LOGIN_FORM_SCHEMA,
|
|
errors={"base": "no_panel_online"},
|
|
)
|
|
|
|
# Show the panel selection.
|
|
# We want the user to choose the panel using the associated name, we set up a mapping
|
|
# dictionary to handle that case.
|
|
panel_names: dict[str, str] = {}
|
|
username = client.get_authenticated_username()
|
|
for panel in online_panels:
|
|
_store_panel_by_name(
|
|
panel=panel, username=username, panel_names=panel_names
|
|
)
|
|
|
|
self._client = client
|
|
self._panel_names = panel_names
|
|
schema = vol.Schema(
|
|
{
|
|
vol.Required(CONF_ELMAX_PANEL_NAME): vol.In(self._panel_names.keys()),
|
|
vol.Required(CONF_ELMAX_PANEL_PIN, default="000000"): str,
|
|
}
|
|
)
|
|
self._panels_schema = schema
|
|
self._username = username
|
|
self._password = password
|
|
# If everything went OK, proceed to panel selection.
|
|
return await self.async_step_panels(user_input=None)
|
|
|
|
async def async_step_panels(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> FlowResult:
|
|
"""Handle Panel selection step."""
|
|
errors: dict[str, Any] = {}
|
|
if user_input is None:
|
|
return self.async_show_form(
|
|
step_id="panels", data_schema=self._panels_schema, errors=errors
|
|
)
|
|
|
|
panel_name = user_input[CONF_ELMAX_PANEL_NAME]
|
|
panel_pin = user_input[CONF_ELMAX_PANEL_PIN]
|
|
|
|
# Lookup the panel id from the panel name.
|
|
panel_id = self._panel_names[panel_name]
|
|
|
|
# Make sure this is the only elmax integration for this specific panel id.
|
|
await self.async_set_unique_id(panel_id)
|
|
self._abort_if_unique_id_configured()
|
|
|
|
# Try to list all the devices using the given PIN.
|
|
try:
|
|
await self._client.get_panel_status(
|
|
control_panel_id=panel_id, pin=panel_pin
|
|
)
|
|
return self.async_create_entry(
|
|
title=f"Elmax {panel_name}",
|
|
data={
|
|
CONF_ELMAX_PANEL_ID: panel_id,
|
|
CONF_ELMAX_PANEL_PIN: panel_pin,
|
|
CONF_ELMAX_USERNAME: self._username,
|
|
CONF_ELMAX_PASSWORD: self._password,
|
|
},
|
|
)
|
|
except ElmaxBadPinError:
|
|
errors["base"] = "invalid_pin"
|
|
except Exception: # pylint: disable=broad-except
|
|
_LOGGER.exception("Error occurred")
|
|
errors["base"] = "unknown"
|
|
|
|
return self.async_show_form(
|
|
step_id="panels", data_schema=self._panels_schema, errors=errors
|
|
)
|
|
|
|
async def async_step_reauth(self, user_input=None):
|
|
"""Perform reauth upon an API authentication error."""
|
|
self._reauth_username = user_input.get(CONF_ELMAX_USERNAME)
|
|
self._reauth_panelid = user_input.get(CONF_ELMAX_PANEL_ID)
|
|
return await self.async_step_reauth_confirm()
|
|
|
|
async def async_step_reauth_confirm(self, user_input=None):
|
|
"""Handle reauthorization flow."""
|
|
errors = {}
|
|
if user_input is not None:
|
|
panel_pin = user_input.get(CONF_ELMAX_PANEL_PIN)
|
|
password = user_input.get(CONF_ELMAX_PASSWORD)
|
|
entry = await self.async_set_unique_id(self._reauth_panelid)
|
|
|
|
# Handle authentication, make sure the panel we are re-authenticating against is listed among results
|
|
# and verify its pin is correct.
|
|
try:
|
|
# Test login.
|
|
client = await self._async_login(
|
|
username=self._reauth_username, password=password
|
|
)
|
|
|
|
# Make sure the panel we are authenticating to is still available.
|
|
panels = [
|
|
p
|
|
for p in await client.list_control_panels()
|
|
if p.hash == self._reauth_panelid
|
|
]
|
|
if len(panels) < 1:
|
|
raise NoOnlinePanelsError()
|
|
|
|
# Verify the pin is still valid.from
|
|
await client.get_panel_status(
|
|
control_panel_id=self._reauth_panelid, pin=panel_pin
|
|
)
|
|
|
|
# If it is, proceed with configuration update.
|
|
self.hass.config_entries.async_update_entry(
|
|
entry,
|
|
data={
|
|
CONF_ELMAX_PANEL_ID: self._reauth_panelid,
|
|
CONF_ELMAX_PANEL_PIN: panel_pin,
|
|
CONF_ELMAX_USERNAME: self._reauth_username,
|
|
CONF_ELMAX_PASSWORD: password,
|
|
},
|
|
)
|
|
await self.hass.config_entries.async_reload(entry.entry_id)
|
|
self._reauth_username = None
|
|
self._reauth_panelid = None
|
|
return self.async_abort(reason="reauth_successful")
|
|
|
|
except ElmaxBadLoginError:
|
|
_LOGGER.error(
|
|
"Wrong credentials or failed login while re-authenticating"
|
|
)
|
|
errors["base"] = "invalid_auth"
|
|
except NoOnlinePanelsError:
|
|
_LOGGER.warning(
|
|
"Panel ID %s is no longer associated to this user",
|
|
self._reauth_panelid,
|
|
)
|
|
errors["base"] = "reauth_panel_disappeared"
|
|
except ElmaxBadPinError:
|
|
errors["base"] = "invalid_pin"
|
|
|
|
# We want the user to re-authenticate only for the given panel id using the same login.
|
|
# We pin them to the UI, so the user realizes she must log in with the appropriate credentials
|
|
# for the that specific panel.
|
|
schema = vol.Schema(
|
|
{
|
|
vol.Required(CONF_ELMAX_USERNAME): self._reauth_username,
|
|
vol.Required(CONF_ELMAX_PASSWORD): str,
|
|
vol.Required(CONF_ELMAX_PANEL_ID): self._reauth_panelid,
|
|
vol.Required(CONF_ELMAX_PANEL_PIN): str,
|
|
}
|
|
)
|
|
return self.async_show_form(
|
|
step_id="reauth_confirm", data_schema=schema, errors=errors
|
|
)
|
|
|
|
@staticmethod
|
|
async def _async_login(username: str, password: str) -> Elmax:
|
|
"""Log in to the Elmax cloud and return the http client."""
|
|
client = Elmax(username=username, password=password)
|
|
await client.login()
|
|
return client
|
|
|
|
|
|
class NoOnlinePanelsError(HomeAssistantError):
|
|
"""Error occurring when no online panel was found."""
|