mirror of https://github.com/home-assistant/core
Add bring integration (#108027)
* add bring integration * fix typings and remove from strictly typed - wait for python-bring-api to be ready for strictly typed * make entity unique to user and list - before it was only list, therefore the same list imported by two users would have failed * simplify bring attribute Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * cleanup and code simplification * remove empty fields in manifest * __init__.py aktualisieren Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * __init__.py aktualisieren Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * strings.json aktualisieren Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * streamline async calls * use coordinator refresh * fix order in update call and simplify bring list * simplify the config_flow * Update homeassistant/components/bring/manifest.json Co-authored-by: Sid <27780930+autinerd@users.noreply.github.com> * add unit testing for __init__.py * cleanup comments * use dict instead of list * Update homeassistant/components/bring/todo.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * clean up * update attribute name * update more attribute name * improve unit tests - remove patch and use mock in conftest * clean up tests even more * more unit test inprovements * remove optional type * minor unit test cleanup * Update .coveragerc Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> Co-authored-by: Sid <27780930+autinerd@users.noreply.github.com>
This commit is contained in:
parent
d631cad07f
commit
f82fb63dce
|
@ -150,6 +150,8 @@ omit =
|
|||
homeassistant/components/braviatv/coordinator.py
|
||||
homeassistant/components/braviatv/media_player.py
|
||||
homeassistant/components/braviatv/remote.py
|
||||
homeassistant/components/bring/coordinator.py
|
||||
homeassistant/components/bring/todo.py
|
||||
homeassistant/components/broadlink/climate.py
|
||||
homeassistant/components/broadlink/light.py
|
||||
homeassistant/components/broadlink/remote.py
|
||||
|
|
|
@ -180,6 +180,8 @@ build.json @home-assistant/supervisor
|
|||
/tests/components/bosch_shc/ @tschamm
|
||||
/homeassistant/components/braviatv/ @bieniu @Drafteed
|
||||
/tests/components/braviatv/ @bieniu @Drafteed
|
||||
/homeassistant/components/bring/ @miaucl @tr4nt0r
|
||||
/tests/components/bring/ @miaucl @tr4nt0r
|
||||
/homeassistant/components/broadlink/ @danielhiversen @felipediel @L-I-Am @eifinger
|
||||
/tests/components/broadlink/ @danielhiversen @felipediel @L-I-Am @eifinger
|
||||
/homeassistant/components/brother/ @bieniu
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
"""The Bring! integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from python_bring_api.bring import Bring
|
||||
from python_bring_api.exceptions import (
|
||||
BringAuthException,
|
||||
BringParseException,
|
||||
BringRequestException,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import BringDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.TODO]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Bring! from a config entry."""
|
||||
|
||||
email = entry.data[CONF_EMAIL]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
|
||||
bring = Bring(email, password)
|
||||
|
||||
def login_and_load_lists() -> None:
|
||||
bring.login()
|
||||
bring.loadLists()
|
||||
|
||||
try:
|
||||
await hass.async_add_executor_job(login_and_load_lists)
|
||||
except BringRequestException as e:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Timeout while connecting for email '{email}'"
|
||||
) from e
|
||||
except BringAuthException as e:
|
||||
_LOGGER.error(
|
||||
"Authentication failed for '%s', check your email and password",
|
||||
email,
|
||||
)
|
||||
raise ConfigEntryError(
|
||||
f"Authentication failed for '{email}', check your email and password"
|
||||
) from e
|
||||
except BringParseException as e:
|
||||
_LOGGER.error(
|
||||
"Failed to parse request '%s', check your email and password",
|
||||
email,
|
||||
)
|
||||
raise ConfigEntryNotReady(
|
||||
"Failed to parse response request from server, try again later"
|
||||
) from e
|
||||
|
||||
coordinator = BringDataUpdateCoordinator(hass, bring)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
|
@ -0,0 +1,75 @@
|
|||
"""Config flow for Bring! integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from python_bring_api.bring import Bring
|
||||
from python_bring_api.exceptions import BringAuthException, BringRequestException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_EMAIL): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.EMAIL,
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD,
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Bring!."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
bring = Bring(user_input[CONF_EMAIL], user_input[CONF_PASSWORD])
|
||||
|
||||
def login_and_load_lists() -> None:
|
||||
bring.login()
|
||||
bring.loadLists()
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(login_and_load_lists)
|
||||
except BringRequestException:
|
||||
errors["base"] = "cannot_connect"
|
||||
except BringAuthException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(bring.uuid)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_EMAIL], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
"""Constants for the Bring! integration."""
|
||||
|
||||
DOMAIN = "bring"
|
|
@ -0,0 +1,66 @@
|
|||
"""DataUpdateCoordinator for the Bring! integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from python_bring_api.bring import Bring
|
||||
from python_bring_api.exceptions import BringParseException, BringRequestException
|
||||
from python_bring_api.types import BringItemsResponse, BringList
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BringData(BringList):
|
||||
"""Coordinator data class."""
|
||||
|
||||
items: list[BringItemsResponse]
|
||||
|
||||
|
||||
class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
|
||||
"""A Bring Data Update Coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, bring: Bring) -> None:
|
||||
"""Initialize the Bring data coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=90),
|
||||
)
|
||||
self.bring = bring
|
||||
|
||||
async def _async_update_data(self) -> dict[str, BringData]:
|
||||
try:
|
||||
lists_response = await self.hass.async_add_executor_job(
|
||||
self.bring.loadLists
|
||||
)
|
||||
except BringRequestException as e:
|
||||
raise UpdateFailed("Unable to connect and retrieve data from bring") from e
|
||||
except BringParseException as e:
|
||||
raise UpdateFailed("Unable to parse response from bring") from e
|
||||
|
||||
list_dict = {}
|
||||
for lst in lists_response["lists"]:
|
||||
try:
|
||||
items = await self.hass.async_add_executor_job(
|
||||
self.bring.getItems, lst["listUuid"]
|
||||
)
|
||||
except BringRequestException as e:
|
||||
raise UpdateFailed(
|
||||
"Unable to connect and retrieve data from bring"
|
||||
) from e
|
||||
except BringParseException as e:
|
||||
raise UpdateFailed("Unable to parse response from bring") from e
|
||||
lst["items"] = items["purchase"]
|
||||
list_dict[lst["listUuid"]] = lst
|
||||
|
||||
return list_dict
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"domain": "bring",
|
||||
"name": "Bring!",
|
||||
"codeowners": ["@miaucl", "@tr4nt0r"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/bring",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["python-bring-api==2.0.0"]
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"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_service%]"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
"""Todo platform for the Bring! integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from python_bring_api.exceptions import BringRequestException
|
||||
|
||||
from homeassistant.components.todo import (
|
||||
TodoItem,
|
||||
TodoItemStatus,
|
||||
TodoListEntity,
|
||||
TodoListEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import BringData, BringDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor from a config entry created in the integrations UI."""
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
unique_id = config_entry.unique_id
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert unique_id
|
||||
|
||||
async_add_entities(
|
||||
BringTodoListEntity(
|
||||
coordinator,
|
||||
bring_list=bring_list,
|
||||
unique_id=unique_id,
|
||||
)
|
||||
for bring_list in coordinator.data.values()
|
||||
)
|
||||
|
||||
|
||||
class BringTodoListEntity(
|
||||
CoordinatorEntity[BringDataUpdateCoordinator], TodoListEntity
|
||||
):
|
||||
"""A To-do List representation of the Bring! Shopping List."""
|
||||
|
||||
_attr_icon = "mdi:cart"
|
||||
_attr_has_entity_name = True
|
||||
_attr_supported_features = (
|
||||
TodoListEntityFeature.CREATE_TODO_ITEM
|
||||
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
| TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: BringDataUpdateCoordinator,
|
||||
bring_list: BringData,
|
||||
unique_id: str,
|
||||
) -> None:
|
||||
"""Initialize BringTodoListEntity."""
|
||||
super().__init__(coordinator)
|
||||
self._list_uuid = bring_list["listUuid"]
|
||||
self._attr_name = bring_list["name"]
|
||||
self._attr_unique_id = f"{unique_id}_{self._list_uuid}"
|
||||
|
||||
@property
|
||||
def todo_items(self) -> list[TodoItem]:
|
||||
"""Return the todo items."""
|
||||
return [
|
||||
TodoItem(
|
||||
uid=item["name"],
|
||||
summary=item["name"],
|
||||
description=item["specification"] or "",
|
||||
status=TodoItemStatus.NEEDS_ACTION,
|
||||
)
|
||||
for item in self.bring_list["items"]
|
||||
]
|
||||
|
||||
@property
|
||||
def bring_list(self) -> BringData:
|
||||
"""Return the bring list."""
|
||||
return self.coordinator.data[self._list_uuid]
|
||||
|
||||
async def async_create_todo_item(self, item: TodoItem) -> None:
|
||||
"""Add an item to the To-do list."""
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.saveItem,
|
||||
self.bring_list["listUuid"],
|
||||
item.summary,
|
||||
item.description or "",
|
||||
)
|
||||
except BringRequestException as e:
|
||||
raise HomeAssistantError("Unable to save todo item for bring") from e
|
||||
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_update_todo_item(self, item: TodoItem) -> None:
|
||||
"""Update an item to the To-do list.
|
||||
|
||||
Bring has an internal 'recent' list which we want to use instead of a todo list
|
||||
status, therefore completed todo list items will directly be deleted
|
||||
|
||||
This results in following behaviour:
|
||||
|
||||
- Completed items will move to the "completed" section in home assistant todo
|
||||
list and get deleted in bring, which will remove them from the home
|
||||
assistant todo list completely after a short delay
|
||||
- Bring items do not have unique identifiers and are using the
|
||||
name/summery/title. Therefore the name is not to be changed! Should a name
|
||||
be changed anyway, a new item will be created instead and no update for
|
||||
this item is performed and on the next cloud pull update, it will get
|
||||
cleared
|
||||
"""
|
||||
|
||||
bring_list = self.bring_list
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert item.uid
|
||||
|
||||
if item.status == TodoItemStatus.COMPLETED:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.removeItem,
|
||||
bring_list["listUuid"],
|
||||
item.uid,
|
||||
)
|
||||
|
||||
elif item.summary == item.uid:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.updateItem,
|
||||
bring_list["listUuid"],
|
||||
item.uid,
|
||||
item.description or "",
|
||||
)
|
||||
except BringRequestException as e:
|
||||
raise HomeAssistantError("Unable to update todo item for bring") from e
|
||||
else:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.removeItem,
|
||||
bring_list["listUuid"],
|
||||
item.uid,
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.saveItem,
|
||||
bring_list["listUuid"],
|
||||
item.summary,
|
||||
item.description or "",
|
||||
)
|
||||
except BringRequestException as e:
|
||||
raise HomeAssistantError("Unable to replace todo item for bring") from e
|
||||
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_delete_todo_items(self, uids: list[str]) -> None:
|
||||
"""Delete an item from the To-do list."""
|
||||
for uid in uids:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.removeItem, self.bring_list["listUuid"], uid
|
||||
)
|
||||
except BringRequestException as e:
|
||||
raise HomeAssistantError("Unable to delete todo item for bring") from e
|
||||
|
||||
await self.coordinator.async_refresh()
|
|
@ -77,6 +77,7 @@ FLOWS = {
|
|||
"bond",
|
||||
"bosch_shc",
|
||||
"braviatv",
|
||||
"bring",
|
||||
"broadlink",
|
||||
"brother",
|
||||
"brottsplatskartan",
|
||||
|
|
|
@ -732,6 +732,12 @@
|
|||
"integration_type": "virtual",
|
||||
"supported_by": "motion_blinds"
|
||||
},
|
||||
"bring": {
|
||||
"name": "Bring!",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"broadlink": {
|
||||
"name": "Broadlink",
|
||||
"integration_type": "hub",
|
||||
|
|
|
@ -2177,6 +2177,9 @@ python-awair==0.2.4
|
|||
# homeassistant.components.blockchain
|
||||
python-blockchain-api==0.0.2
|
||||
|
||||
# homeassistant.components.bring
|
||||
python-bring-api==2.0.0
|
||||
|
||||
# homeassistant.components.bsblan
|
||||
python-bsblan==0.5.18
|
||||
|
||||
|
|
|
@ -1677,6 +1677,9 @@ python-MotionMount==0.3.1
|
|||
# homeassistant.components.awair
|
||||
python-awair==0.2.4
|
||||
|
||||
# homeassistant.components.bring
|
||||
python-bring-api==2.0.0
|
||||
|
||||
# homeassistant.components.bsblan
|
||||
python-bsblan==0.5.18
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the Bring! integration."""
|
|
@ -0,0 +1,49 @@
|
|||
"""Common fixtures for the Bring! tests."""
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.bring import DOMAIN
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
EMAIL = "test-email"
|
||||
PASSWORD = "test-password"
|
||||
|
||||
UUID = "00000000-00000000-00000000-00000000"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[Mock, None, None]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.bring.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_bring_client() -> Generator[Mock, None, None]:
|
||||
"""Mock a Bring client."""
|
||||
with patch(
|
||||
"homeassistant.components.bring.Bring",
|
||||
autospec=True,
|
||||
) as mock_client, patch(
|
||||
"homeassistant.components.bring.config_flow.Bring",
|
||||
new=mock_client,
|
||||
):
|
||||
client = mock_client.return_value
|
||||
client.uuid = UUID
|
||||
client.login.return_value = True
|
||||
client.loadLists.return_value = {"lists": []}
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture(name="bring_config_entry")
|
||||
def mock_bring_config_entry() -> MockConfigEntry:
|
||||
"""Mock bring configuration entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD}, unique_id=UUID
|
||||
)
|
|
@ -0,0 +1,111 @@
|
|||
"""Test the Bring! config flow."""
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
from python_bring_api.exceptions import (
|
||||
BringAuthException,
|
||||
BringParseException,
|
||||
BringRequestException,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bring.const import DOMAIN
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from .conftest import EMAIL, PASSWORD
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
MOCK_DATA_STEP = {
|
||||
CONF_EMAIL: EMAIL,
|
||||
CONF_PASSWORD: PASSWORD,
|
||||
}
|
||||
|
||||
|
||||
async def test_form(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_bring_client: Mock
|
||||
) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "user"}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_DATA_STEP,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == MOCK_DATA_STEP["email"]
|
||||
assert result["data"] == MOCK_DATA_STEP
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raise_error", "text_error"),
|
||||
[
|
||||
(BringRequestException(), "cannot_connect"),
|
||||
(BringAuthException(), "invalid_auth"),
|
||||
(BringParseException(), "unknown"),
|
||||
(IndexError(), "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_flow_user_init_data_unknown_error_and_recover(
|
||||
hass: HomeAssistant, mock_bring_client: Mock, raise_error, text_error
|
||||
) -> None:
|
||||
"""Test unknown errors."""
|
||||
mock_bring_client.login.side_effect = raise_error
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "user"}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_DATA_STEP,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"]["base"] == text_error
|
||||
|
||||
# Recover
|
||||
mock_bring_client.login.side_effect = None
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "user"}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_DATA_STEP,
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["result"].title == MOCK_DATA_STEP["email"]
|
||||
|
||||
assert result["data"] == MOCK_DATA_STEP
|
||||
|
||||
|
||||
async def test_flow_user_init_data_already_configured(
|
||||
hass: HomeAssistant, mock_bring_client: Mock, bring_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test we abort user data set when entry is already configured."""
|
||||
|
||||
bring_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "user"}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_DATA_STEP,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
|
@ -0,0 +1,63 @@
|
|||
"""Unit tests for the bring integration."""
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.bring import (
|
||||
BringAuthException,
|
||||
BringParseException,
|
||||
BringRequestException,
|
||||
)
|
||||
from homeassistant.components.bring.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_integration(
|
||||
hass: HomeAssistant,
|
||||
bring_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Mock setup of the bring integration."""
|
||||
bring_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(bring_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_load_unload(
|
||||
hass: HomeAssistant,
|
||||
mock_bring_client: Mock,
|
||||
bring_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test loading and unloading of the config entry."""
|
||||
await setup_integration(hass, bring_config_entry)
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
|
||||
assert bring_config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(bring_config_entry.entry_id)
|
||||
assert bring_config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "status"),
|
||||
[
|
||||
(BringRequestException, ConfigEntryState.SETUP_RETRY),
|
||||
(BringAuthException, ConfigEntryState.SETUP_ERROR),
|
||||
(BringParseException, ConfigEntryState.SETUP_RETRY),
|
||||
],
|
||||
)
|
||||
async def test_init_failure(
|
||||
hass: HomeAssistant,
|
||||
mock_bring_client: Mock,
|
||||
status: ConfigEntryState,
|
||||
exception: Exception,
|
||||
bring_config_entry: MockConfigEntry | None,
|
||||
) -> None:
|
||||
"""Test an initialization error on integration load."""
|
||||
mock_bring_client.login.side_effect = exception
|
||||
await setup_integration(hass, bring_config_entry)
|
||||
assert bring_config_entry.state == status
|
Loading…
Reference in New Issue