1
mirror of https://github.com/home-assistant/core synced 2024-08-28 03:36:46 +02:00
ha-core/homeassistant/components/life360/config_flow.py
2023-08-29 16:38:11 +02:00

207 lines
7.3 KiB
Python

"""Config flow to configure Life360 integration."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any, cast
from life360 import Life360, Life360Error, LoginError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from .const import (
COMM_TIMEOUT,
CONF_AUTHORIZATION,
CONF_DRIVING_SPEED,
CONF_MAX_GPS_ACCURACY,
DEFAULT_OPTIONS,
DOMAIN,
LOGGER,
OPTIONS,
SHOW_DRIVING,
)
LIMIT_GPS_ACC = "limit_gps_acc"
SET_DRIVE_SPEED = "set_drive_speed"
def account_schema(
def_username: str | vol.UNDEFINED = vol.UNDEFINED,
def_password: str | vol.UNDEFINED = vol.UNDEFINED,
) -> dict[vol.Marker, Any]:
"""Return schema for an account with optional default values."""
return {
vol.Required(CONF_USERNAME, default=def_username): cv.string,
vol.Required(CONF_PASSWORD, default=def_password): cv.string,
}
def password_schema(
def_password: str | vol.UNDEFINED = vol.UNDEFINED,
) -> dict[vol.Marker, Any]:
"""Return schema for a password with optional default value."""
return {vol.Required(CONF_PASSWORD, default=def_password): cv.string}
class Life360ConfigFlow(ConfigFlow, domain=DOMAIN):
"""Life360 integration config flow."""
VERSION = 1
_api: Life360 | None = None
_username: str | vol.UNDEFINED = vol.UNDEFINED
_password: str | vol.UNDEFINED = vol.UNDEFINED
_reauth_entry: ConfigEntry | None = None
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> Life360OptionsFlow:
"""Get the options flow for this handler."""
return Life360OptionsFlow(config_entry)
async def _async_verify(self, step_id: str) -> FlowResult:
"""Attempt to authorize the provided credentials."""
if not self._api:
self._api = Life360(
session=async_get_clientsession(self.hass), timeout=COMM_TIMEOUT
)
errors: dict[str, str] = {}
try:
authorization = await self._api.get_authorization(
self._username, self._password
)
except LoginError as exc:
LOGGER.debug("Login error: %s", exc)
errors["base"] = "invalid_auth"
except Life360Error as exc:
LOGGER.debug("Unexpected error communicating with Life360 server: %s", exc)
errors["base"] = "cannot_connect"
if errors:
if step_id == "user":
schema = account_schema(self._username, self._password)
else:
schema = password_schema(self._password)
return self.async_show_form(
step_id=step_id, data_schema=vol.Schema(schema), errors=errors
)
data = {
CONF_USERNAME: self._username,
CONF_PASSWORD: self._password,
CONF_AUTHORIZATION: authorization,
}
if self._reauth_entry:
LOGGER.debug("Reauthorization successful")
self.hass.config_entries.async_update_entry(self._reauth_entry, data=data)
self.hass.async_create_task(
self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(
title=cast(str, self.unique_id), data=data, options=DEFAULT_OPTIONS
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a config flow initiated by the user."""
if not user_input:
return self.async_show_form(
step_id="user", data_schema=vol.Schema(account_schema())
)
self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD]
await self.async_set_unique_id(self._username.lower())
self._abort_if_unique_id_configured()
return await self._async_verify("user")
async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult:
"""Handle reauthorization."""
self._username = data[CONF_USERNAME]
self._reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
# Always start with current credentials since they may still be valid and a
# simple reauthorization will be successful.
return await self.async_step_reauth_confirm(dict(data))
async def async_step_reauth_confirm(self, user_input: dict[str, Any]) -> FlowResult:
"""Handle reauthorization completion."""
if not user_input:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(password_schema(self._password)),
errors={"base": "invalid_auth"},
)
self._password = user_input[CONF_PASSWORD]
return await self._async_verify("reauth_confirm")
class Life360OptionsFlow(OptionsFlow):
"""Life360 integration options flow."""
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize."""
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle account options."""
options = self.config_entry.options
if user_input is not None:
new_options = _extract_account_options(user_input)
return self.async_create_entry(title="", data=new_options)
return self.async_show_form(
step_id="init", data_schema=vol.Schema(_account_options_schema(options))
)
def _account_options_schema(options: Mapping[str, Any]) -> dict[vol.Marker, Any]:
"""Create schema for account options form."""
def_limit_gps_acc = options[CONF_MAX_GPS_ACCURACY] is not None
def_max_gps = options[CONF_MAX_GPS_ACCURACY] or vol.UNDEFINED
def_set_drive_speed = options[CONF_DRIVING_SPEED] is not None
def_speed = options[CONF_DRIVING_SPEED] or vol.UNDEFINED
def_show_driving = options[SHOW_DRIVING]
return {
vol.Required(LIMIT_GPS_ACC, default=def_limit_gps_acc): bool,
vol.Optional(CONF_MAX_GPS_ACCURACY, default=def_max_gps): vol.Coerce(float),
vol.Required(SET_DRIVE_SPEED, default=def_set_drive_speed): bool,
vol.Optional(CONF_DRIVING_SPEED, default=def_speed): vol.Coerce(float),
vol.Optional(SHOW_DRIVING, default=def_show_driving): bool,
}
def _extract_account_options(user_input: dict) -> dict[str, Any]:
"""Remove options from user input and return as a separate dict."""
result = {}
for key in OPTIONS:
value = user_input.pop(key, None)
# Was "include" checkbox (if there was one) corresponding to option key True
# (meaning option should be included)?
incl = user_input.pop(
{
CONF_MAX_GPS_ACCURACY: LIMIT_GPS_ACC,
CONF_DRIVING_SPEED: SET_DRIVE_SPEED,
}.get(key),
True,
)
result[key] = value if incl else None
return result