Brunt package update with async, data update coordinator and config flow (#49714)

* implemented config_flow and dataupdatecoordinator

* implemented config flow, dataupdatecoordinator and tests.

* undid extra vscode task

* fixed pylint errors

* updates based on review

* fix mypy in reauth

* fast interval to 5 sec

* fixed test patches and others from review

* added released package

* deleted wrong line from coveragerc

* updates to config and tests

* fixed test patch
This commit is contained in:
Eduard van Valkenburg 2021-11-18 23:00:42 +01:00 committed by GitHub
parent 05eb2f3e5c
commit 958c199d80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 586 additions and 79 deletions

View file

@ -138,7 +138,9 @@ omit =
homeassistant/components/broadlink/updater.py homeassistant/components/broadlink/updater.py
homeassistant/components/brottsplatskartan/sensor.py homeassistant/components/brottsplatskartan/sensor.py
homeassistant/components/browser/* homeassistant/components/browser/*
homeassistant/components/brunt/__init__.py
homeassistant/components/brunt/cover.py homeassistant/components/brunt/cover.py
homeassistant/components/brunt/const.py
homeassistant/components/bsblan/climate.py homeassistant/components/bsblan/climate.py
homeassistant/components/bt_home_hub_5/device_tracker.py homeassistant/components/bt_home_hub_5/device_tracker.py
homeassistant/components/bt_smarthub/device_tracker.py homeassistant/components/bt_smarthub/device_tracker.py

View file

@ -1 +1,78 @@
"""The brunt component.""" """The brunt component."""
from __future__ import annotations
import logging
from aiohttp.client_exceptions import ClientResponseError, ServerDisconnectedError
import async_timeout
from brunt import BruntClientAsync
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DATA_BAPI, DATA_COOR, DOMAIN, PLATFORMS, REGULAR_INTERVAL
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Brunt using config flow."""
session = async_get_clientsession(hass)
bapi = BruntClientAsync(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
session=session,
)
try:
await bapi.async_login()
except ServerDisconnectedError as exc:
raise ConfigEntryNotReady("Brunt not ready to connect.") from exc
except ClientResponseError as exc:
raise ConfigEntryAuthFailed(
f"Brunt could not connect with username: {entry.data[CONF_USERNAME]}."
) from exc
async def async_update_data():
"""Fetch data from the Brunt endpoint for all Things.
Error 403 is the API response for any kind of authentication error (failed password or email)
Error 401 is the API response for things that are not part of the account, could happen when a device is deleted from the account.
"""
try:
async with async_timeout.timeout(10):
things = await bapi.async_get_things(force=True)
return {thing.SERIAL: thing for thing in things}
except ServerDisconnectedError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
except ClientResponseError as err:
if err.status == 403:
raise ConfigEntryAuthFailed() from err
if err.status == 401:
_LOGGER.warning("Device not found, will reload Brunt integration")
await hass.config_entries.async_reload(entry.entry_id)
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name="brunt",
update_method=async_update_data,
update_interval=REGULAR_INTERVAL,
)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {DATA_BAPI: bapi, DATA_COOR: coordinator}
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View file

@ -0,0 +1,119 @@
"""Config flow for brunt integration."""
from __future__ import annotations
import logging
from typing import Any
from aiohttp import ClientResponseError
from aiohttp.client_exceptions import ServerDisconnectedError
from brunt import BruntClientAsync
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
)
REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
async def validate_input(user_input: dict[str, Any]) -> dict[str, str] | None:
"""Login to the brunt api and return errors if any."""
errors = None
bapi = BruntClientAsync(
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
)
try:
await bapi.async_login()
except ClientResponseError as exc:
if exc.status == 403:
_LOGGER.warning("Brunt Credentials are incorrect")
errors = {"base": "invalid_auth"}
else:
_LOGGER.exception("Unknown error when trying to login to Brunt: %s", exc)
errors = {"base": "unknown"}
except ServerDisconnectedError:
_LOGGER.warning("Cannot connect to Brunt")
errors = {"base": "cannot_connect"}
except Exception as exc: # pylint: disable=broad-except
_LOGGER.exception("Unknown error when trying to login to Brunt: %s", exc)
errors = {"base": "unknown"}
finally:
await bapi.async_close()
return errors
class BruntConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Brunt."""
VERSION = 1
_reauth_entry: ConfigEntry | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
errors = await validate_input(user_input)
if errors is not None:
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
await self.async_set_unique_id(user_input[CONF_USERNAME].lower())
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_USERNAME],
data=user_input,
)
async def async_step_reauth(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Perform reauth upon an API authentication error."""
self._reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Dialog that informs the user that reauth is required."""
assert self._reauth_entry
username = self._reauth_entry.data[CONF_USERNAME]
if user_input is None:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=REAUTH_SCHEMA,
description_placeholders={"username": username},
)
user_input[CONF_USERNAME] = username
errors = await validate_input(user_input)
if errors is not None:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=REAUTH_SCHEMA,
errors=errors,
description_placeholders={"username": username},
)
self.hass.config_entries.async_update_entry(self._reauth_entry, data=user_input)
await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
return self.async_abort(reason="reauth_successful")
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
"""Import config from configuration.yaml."""
await self.async_set_unique_id(import_config[CONF_USERNAME].lower())
self._abort_if_unique_id_configured()
return await self.async_step_user(import_config)

View file

@ -0,0 +1,17 @@
"""Constants for Brunt."""
from datetime import timedelta
DOMAIN = "brunt"
ATTR_REQUEST_POSITION = "request_position"
NOTIFICATION_ID = "brunt_notification"
NOTIFICATION_TITLE = "Brunt Cover Setup"
ATTRIBUTION = "Based on an unofficial Brunt SDK."
PLATFORMS = ["cover"]
DATA_BAPI = "bapi"
DATA_COOR = "coordinator"
CLOSED_POSITION = 0
OPEN_POSITION = 100
REGULAR_INTERVAL = timedelta(seconds=20)
FAST_INTERVAL = timedelta(seconds=5)

View file

@ -1,84 +1,134 @@
"""Support for Brunt Blind Engine covers.""" """Support for Brunt Blind Engine covers."""
from __future__ import annotations from __future__ import annotations
from collections.abc import MutableMapping
import logging import logging
from typing import Any
from brunt import BruntAPI from aiohttp.client_exceptions import ClientResponseError
import voluptuous as vol from brunt import BruntClientAsync, Thing
from homeassistant.components.cover import ( from homeassistant.components.cover import (
ATTR_POSITION, ATTR_POSITION,
DEVICE_CLASS_WINDOW, DEVICE_CLASS_SHADE,
PLATFORM_SCHEMA,
SUPPORT_CLOSE, SUPPORT_CLOSE,
SUPPORT_OPEN, SUPPORT_OPEN,
SUPPORT_SET_POSITION, SUPPORT_SET_POSITION,
CoverEntity, CoverEntity,
) )
from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
import homeassistant.helpers.config_validation as cv from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from .const import (
ATTR_REQUEST_POSITION,
ATTRIBUTION,
CLOSED_POSITION,
DATA_BAPI,
DATA_COOR,
DOMAIN,
FAST_INTERVAL,
OPEN_POSITION,
REGULAR_INTERVAL,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
COVER_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION COVER_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
ATTR_REQUEST_POSITION = "request_position"
NOTIFICATION_ID = "brunt_notification"
NOTIFICATION_TITLE = "Brunt Cover Setup"
ATTRIBUTION = "Based on an unofficial Brunt SDK."
CLOSED_POSITION = 0 async def async_setup_platform(
OPEN_POSITION = 100 hass: HomeAssistant,
config: ConfigType,
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( add_entities: AddEntitiesCallback,
{vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} discovery_info: DiscoveryInfoType | None = None,
) ) -> None:
"""Component setup, run import config flow for each entry in config."""
_LOGGER.warning(
def setup_platform(hass, config, add_entities, discovery_info=None): "Loading brunt via platform config is deprecated; The configuration has been migrated to a config entry and can be safely removed from configuration.yaml"
"""Set up the brunt platform.""" )
hass.async_create_task(
username = config[CONF_USERNAME] hass.config_entries.flow.async_init(
password = config[CONF_PASSWORD] DOMAIN, context={"source": SOURCE_IMPORT}, data=config
bapi = BruntAPI(username=username, password=password)
try:
if not (things := bapi.getThings()["things"]):
_LOGGER.error("No things present in account")
else:
add_entities(
[
BruntDevice(bapi, thing["NAME"], thing["thingUri"])
for thing in things
],
True,
)
except (TypeError, KeyError, NameError, ValueError) as ex:
_LOGGER.error("%s", ex)
hass.components.persistent_notification.create(
"Error: {ex}<br />You will need to restart hass after fixing.",
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID,
) )
)
class BruntDevice(CoverEntity): async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the brunt platform."""
bapi: BruntClientAsync = hass.data[DOMAIN][entry.entry_id][DATA_BAPI]
coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][DATA_COOR]
async_add_entities(
BruntDevice(coordinator, serial, thing, bapi, entry.entry_id)
for serial, thing in coordinator.data.items()
)
class BruntDevice(CoordinatorEntity, CoverEntity):
""" """
Representation of a Brunt cover device. Representation of a Brunt cover device.
Contains the common logic for all Brunt devices. Contains the common logic for all Brunt devices.
""" """
_attr_device_class = DEVICE_CLASS_WINDOW def __init__(
_attr_supported_features = COVER_FEATURES self,
coordinator: DataUpdateCoordinator,
def __init__(self, bapi, name, thing_uri): serial: str,
thing: Thing,
bapi: BruntClientAsync,
entry_id: str,
) -> None:
"""Init the Brunt device.""" """Init the Brunt device."""
super().__init__(coordinator)
self._attr_unique_id = serial
self._bapi = bapi self._bapi = bapi
self._attr_name = name self._thing = thing
self._thing_uri = thing_uri self._entry_id = entry_id
self._state = {} self._remove_update_listener = None
self._attr_name = self._thing.NAME
self._attr_device_class = DEVICE_CLASS_SHADE
self._attr_supported_features = COVER_FEATURES
self._attr_attribution = ATTRIBUTION
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
name=self._attr_name,
via_device=(DOMAIN, self._entry_id),
manufacturer="Brunt",
sw_version=self._thing.FW_VERSION,
model=self._thing.MODEL,
)
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self.async_on_remove(
self.coordinator.async_add_listener(self._brunt_update_listener)
)
@property
def current_cover_position(self) -> int | None:
"""
Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
pos = self.coordinator.data[self.unique_id].currentPosition
return int(pos) if pos is not None else None
@property @property
def request_cover_position(self) -> int | None: def request_cover_position(self) -> int | None:
@ -89,8 +139,8 @@ class BruntDevice(CoverEntity):
to Brunt, at times there is a diff of 1 to current to Brunt, at times there is a diff of 1 to current
None is unknown, 0 is closed, 100 is fully open. None is unknown, 0 is closed, 100 is fully open.
""" """
pos = self._state.get("requestPosition") pos = self.coordinator.data[self.unique_id].requestPosition
return int(pos) if pos else None return int(pos) if pos is not None else None
@property @property
def move_state(self) -> int | None: def move_state(self) -> int | None:
@ -99,37 +149,64 @@ class BruntDevice(CoverEntity):
None is unknown, 0 when stopped, 1 when opening, 2 when closing None is unknown, 0 when stopped, 1 when opening, 2 when closing
""" """
mov = self._state.get("moveState") mov = self.coordinator.data[self.unique_id].moveState
return int(mov) if mov else None return int(mov) if mov is not None else None
def update(self): @property
"""Poll the current state of the device.""" def is_opening(self) -> bool:
try: """Return if the cover is opening or not."""
self._state = self._bapi.getState(thingUri=self._thing_uri).get("thing") return self.move_state == 1
self._attr_available = True
except (TypeError, KeyError, NameError, ValueError) as ex: @property
_LOGGER.error("%s", ex) def is_closing(self) -> bool:
self._attr_available = False """Return if the cover is closing or not."""
self._attr_is_opening = self.move_state == 1 return self.move_state == 2
self._attr_is_closing = self.move_state == 2
pos = self._state.get("currentPosition") @property
self._attr_current_cover_position = int(pos) if pos else None def extra_state_attributes(self) -> MutableMapping[str, Any]:
self._attr_is_closed = self.current_cover_position == CLOSED_POSITION """Return the detailed device state attributes."""
self._attr_extra_state_attributes = { return {
ATTR_ATTRIBUTION: ATTRIBUTION,
ATTR_REQUEST_POSITION: self.request_cover_position, ATTR_REQUEST_POSITION: self.request_cover_position,
} }
def open_cover(self, **kwargs): @property
def is_closed(self) -> bool:
"""Return true if cover is closed, else False."""
return self.current_cover_position == CLOSED_POSITION
async def async_open_cover(self, **kwargs: Any) -> None:
"""Set the cover to the open position.""" """Set the cover to the open position."""
self._bapi.changeRequestPosition(OPEN_POSITION, thingUri=self._thing_uri) await self._async_update_cover(OPEN_POSITION)
def close_cover(self, **kwargs): async def async_close_cover(self, **kwargs: Any) -> None:
"""Set the cover to the closed position.""" """Set the cover to the closed position."""
self._bapi.changeRequestPosition(CLOSED_POSITION, thingUri=self._thing_uri) await self._async_update_cover(CLOSED_POSITION)
def set_cover_position(self, **kwargs): async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Set the cover to a specific position.""" """Set the cover to a specific position."""
self._bapi.changeRequestPosition( await self._async_update_cover(int(kwargs[ATTR_POSITION]))
kwargs[ATTR_POSITION], thingUri=self._thing_uri
) async def _async_update_cover(self, position: int) -> None:
"""Set the cover to the new position and wait for the update to be reflected."""
try:
await self._bapi.async_change_request_position(
position, thingUri=self._thing.thingUri
)
except ClientResponseError as exc:
raise HomeAssistantError(
f"Unable to reposition {self._thing.NAME}"
) from exc
self.coordinator.update_interval = FAST_INTERVAL
await self.coordinator.async_request_refresh()
@callback
def _brunt_update_listener(self) -> None:
"""Update the update interval after each refresh."""
if (
self.request_cover_position
== self._bapi.last_requested_positions[self._thing.thingUri]
and self.move_state == 0
):
self.coordinator.update_interval = REGULAR_INTERVAL
else:
self.coordinator.update_interval = FAST_INTERVAL

View file

@ -1,8 +1,9 @@
{ {
"domain": "brunt", "domain": "brunt",
"name": "Brunt Blind Engine", "name": "Brunt Blind Engine",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/brunt", "documentation": "https://www.home-assistant.io/integrations/brunt",
"requirements": ["brunt==0.1.3"], "requirements": ["brunt==1.0.0"],
"codeowners": ["@eavanvalkenburg"], "codeowners": ["@eavanvalkenburg"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View file

@ -0,0 +1,29 @@
{
"config": {
"step": {
"user": {
"title": "Setup your Brunt integration",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "Please reenter the password for: {username}",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
}
}

View file

@ -43,6 +43,7 @@ FLOWS = [
"braviatv", "braviatv",
"broadlink", "broadlink",
"brother", "brother",
"brunt",
"bsblan", "bsblan",
"buienradar", "buienradar",
"canary", "canary",

View file

@ -440,7 +440,7 @@ brother==1.1.0
brottsplatskartan==0.0.1 brottsplatskartan==0.0.1
# homeassistant.components.brunt # homeassistant.components.brunt
brunt==0.1.3 brunt==1.0.0
# homeassistant.components.bsblan # homeassistant.components.bsblan
bsblan==0.4.0 bsblan==0.4.0

View file

@ -277,6 +277,9 @@ broadlink==0.18.0
# homeassistant.components.brother # homeassistant.components.brother
brother==1.1.0 brother==1.1.0
# homeassistant.components.brunt
brunt==1.0.0
# homeassistant.components.bsblan # homeassistant.components.bsblan
bsblan==0.4.0 bsblan==0.4.0

View file

@ -0,0 +1 @@
"""Brunt tests."""

View file

@ -0,0 +1,180 @@
"""Test the Brunt config flow."""
from unittest.mock import Mock, patch
from aiohttp import ClientResponseError
from aiohttp.client_exceptions import ServerDisconnectedError
import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.brunt.const import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from tests.common import MockConfigEntry
CONFIG = {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}
async def test_form(hass):
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None
)
assert result["type"] == "form"
assert result["errors"] is None
with patch(
"homeassistant.components.brunt.config_flow.BruntClientAsync.async_login",
return_value=None,
), patch(
"homeassistant.components.brunt.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
CONFIG,
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "test-username"
assert result2["data"] == CONFIG
assert len(mock_setup_entry.mock_calls) == 1
async def test_import(hass):
"""Test we get the form."""
with patch(
"homeassistant.components.brunt.config_flow.BruntClientAsync.async_login",
return_value=None,
), patch(
"homeassistant.components.brunt.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=CONFIG
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "test-username"
assert result["data"] == CONFIG
assert len(mock_setup_entry.mock_calls) == 1
async def test_import_duplicate_login(hass):
"""Test uniqueness of username."""
entry = MockConfigEntry(
domain=DOMAIN,
data=CONFIG,
title="test-username",
unique_id="test-username",
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.brunt.config_flow.BruntClientAsync.async_login",
return_value=None,
), patch(
"homeassistant.components.brunt.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=CONFIG
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_form_duplicate_login(hass):
"""Test uniqueness of username."""
entry = MockConfigEntry(
domain=DOMAIN,
data=CONFIG,
title="test-username",
unique_id="test-username",
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.brunt.config_flow.BruntClientAsync.async_login",
return_value=None,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
@pytest.mark.parametrize(
"side_effect, error_message",
[
(ServerDisconnectedError, "cannot_connect"),
(ClientResponseError(Mock(), None, status=403), "invalid_auth"),
(ClientResponseError(Mock(), None, status=401), "unknown"),
(Exception, "unknown"),
],
)
async def test_form_error(hass, side_effect, error_message):
"""Test we handle cannot connect."""
with patch(
"homeassistant.components.brunt.config_flow.BruntClientAsync.async_login",
side_effect=side_effect,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": error_message}
@pytest.mark.parametrize(
"side_effect, result_type, password, step_id, reason",
[
(None, data_entry_flow.RESULT_TYPE_ABORT, "test", None, "reauth_successful"),
(
Exception,
data_entry_flow.RESULT_TYPE_FORM,
CONFIG[CONF_PASSWORD],
"reauth_confirm",
None,
),
],
)
async def test_reauth(hass, side_effect, result_type, password, step_id, reason):
"""Test uniqueness of username."""
entry = MockConfigEntry(
domain=DOMAIN,
data=CONFIG,
title="test-username",
unique_id="test-username",
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"unique_id": entry.unique_id,
"entry_id": entry.entry_id,
},
data=None,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth_confirm"
with patch(
"homeassistant.components.brunt.config_flow.BruntClientAsync.async_login",
return_value=None,
side_effect=side_effect,
):
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={"password": "test"},
)
assert result3["type"] == result_type
assert entry.data["password"] == password
assert result3.get("step_id", None) == step_id
assert result3.get("reason", None) == reason