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/coordinator.py
|
||||||
homeassistant/components/braviatv/media_player.py
|
homeassistant/components/braviatv/media_player.py
|
||||||
homeassistant/components/braviatv/remote.py
|
homeassistant/components/braviatv/remote.py
|
||||||
|
homeassistant/components/bring/coordinator.py
|
||||||
|
homeassistant/components/bring/todo.py
|
||||||
homeassistant/components/broadlink/climate.py
|
homeassistant/components/broadlink/climate.py
|
||||||
homeassistant/components/broadlink/light.py
|
homeassistant/components/broadlink/light.py
|
||||||
homeassistant/components/broadlink/remote.py
|
homeassistant/components/broadlink/remote.py
|
||||||
|
|
|
@ -180,6 +180,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/bosch_shc/ @tschamm
|
/tests/components/bosch_shc/ @tschamm
|
||||||
/homeassistant/components/braviatv/ @bieniu @Drafteed
|
/homeassistant/components/braviatv/ @bieniu @Drafteed
|
||||||
/tests/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
|
/homeassistant/components/broadlink/ @danielhiversen @felipediel @L-I-Am @eifinger
|
||||||
/tests/components/broadlink/ @danielhiversen @felipediel @L-I-Am @eifinger
|
/tests/components/broadlink/ @danielhiversen @felipediel @L-I-Am @eifinger
|
||||||
/homeassistant/components/brother/ @bieniu
|
/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",
|
"bond",
|
||||||
"bosch_shc",
|
"bosch_shc",
|
||||||
"braviatv",
|
"braviatv",
|
||||||
|
"bring",
|
||||||
"broadlink",
|
"broadlink",
|
||||||
"brother",
|
"brother",
|
||||||
"brottsplatskartan",
|
"brottsplatskartan",
|
||||||
|
|
|
@ -732,6 +732,12 @@
|
||||||
"integration_type": "virtual",
|
"integration_type": "virtual",
|
||||||
"supported_by": "motion_blinds"
|
"supported_by": "motion_blinds"
|
||||||
},
|
},
|
||||||
|
"bring": {
|
||||||
|
"name": "Bring!",
|
||||||
|
"integration_type": "service",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "cloud_polling"
|
||||||
|
},
|
||||||
"broadlink": {
|
"broadlink": {
|
||||||
"name": "Broadlink",
|
"name": "Broadlink",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
|
|
|
@ -2177,6 +2177,9 @@ python-awair==0.2.4
|
||||||
# homeassistant.components.blockchain
|
# homeassistant.components.blockchain
|
||||||
python-blockchain-api==0.0.2
|
python-blockchain-api==0.0.2
|
||||||
|
|
||||||
|
# homeassistant.components.bring
|
||||||
|
python-bring-api==2.0.0
|
||||||
|
|
||||||
# homeassistant.components.bsblan
|
# homeassistant.components.bsblan
|
||||||
python-bsblan==0.5.18
|
python-bsblan==0.5.18
|
||||||
|
|
||||||
|
|
|
@ -1677,6 +1677,9 @@ python-MotionMount==0.3.1
|
||||||
# homeassistant.components.awair
|
# homeassistant.components.awair
|
||||||
python-awair==0.2.4
|
python-awair==0.2.4
|
||||||
|
|
||||||
|
# homeassistant.components.bring
|
||||||
|
python-bring-api==2.0.0
|
||||||
|
|
||||||
# homeassistant.components.bsblan
|
# homeassistant.components.bsblan
|
||||||
python-bsblan==0.5.18
|
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