From c3a2fce5ccdfe2c777156ce481f8075838cffcc3 Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 28 Jun 2022 17:22:18 -0400 Subject: [PATCH] Move to async for aladdin connect integration (#73954) * Moved to AIOAladdinConnect API * Added callback logic for door status * close unused connections * Close connection after verification * Matched to current version * Matched __init__.py to current release * Matched cover.py to existing version * added missing awaits * Moved callback * Bumped AIOAladdinConnect to 0.1.3 * Removed await from callback config * Finished tests * Added callback test * Bumped AIOAladdinConnect to 0.1.4 * Finished tests * Callback correct call to update HA * Modified calls to state machine * Modified update path * Removed unused status * Bumped AIOAladdinConnect to 0.1.7 * Revised test_cover cover tests and bumped AIOAladdinConnect to 0.1.10 * Bumped AIOAladdinConnect to 0.1.11 * Bumped AIOAladdinConenct to 0.1.12 * Bumped AIOAladdinConnect to 0.1.13 * Bumped AIOAladdinConnect to 0.1.14 * Added ability to handle multiple doors * Added timout errors to config flow * asyncio timout error added to setup retry * Cleanup added to hass proceedure * Bumped AIOAladdinConnect to 0.1.16 * Bumped AIOAladdinConnect to 0.1.18 * Bumped AIOAladdinConnect to 0.1.19 * Bumped AIOAladdinConnect to 0.1.20 * Addressed recommended changes: SCAN_INTERVAL and spelling * Moved to async_get_clientsession and bumped AIOAladdinConnect to 0.1.21 * Missing test for new code structure * removed extra call to write_ha_state, callback decorator, cleaned up tests * Update tests/components/aladdin_connect/test_init.py Co-authored-by: Martin Hjelmare * Removed extra_attributes. * Added typing to variable acc Co-authored-by: Martin Hjelmare --- .../components/aladdin_connect/__init__.py | 17 +- .../components/aladdin_connect/config_flow.py | 21 +- .../components/aladdin_connect/cover.py | 70 ++++-- .../components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/aladdin_connect/conftest.py | 39 +++ .../aladdin_connect/test_config_flow.py | 155 ++++++++---- .../components/aladdin_connect/test_cover.py | 238 +++++++++--------- tests/components/aladdin_connect/test_init.py | 113 +++++++-- 10 files changed, 458 insertions(+), 209 deletions(-) create mode 100644 tests/components/aladdin_connect/conftest.py diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 048624641bd..af996c9f5b2 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -1,13 +1,16 @@ """The aladdin_connect component.""" +import asyncio import logging from typing import Final -from aladdin_connect import AladdinConnectClient +from AIOAladdinConnect import AladdinConnectClient +from aiohttp import ClientConnectionError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -20,9 +23,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up platform from a ConfigEntry.""" username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] - acc = AladdinConnectClient(username, password) - if not await hass.async_add_executor_job(acc.login): - raise ConfigEntryAuthFailed("Incorrect Password") + acc = AladdinConnectClient(username, password, async_get_clientsession(hass)) + try: + if not await acc.login(): + raise ConfigEntryAuthFailed("Incorrect Password") + except (ClientConnectionError, asyncio.TimeoutError) as ex: + raise ConfigEntryNotReady("Can not connect to host") from ex + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 0b928e9d423..0d45ea9a8ef 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -1,11 +1,14 @@ """Config flow for Aladdin Connect cover integration.""" from __future__ import annotations +import asyncio from collections.abc import Mapping import logging from typing import Any -from aladdin_connect import AladdinConnectClient +from AIOAladdinConnect import AladdinConnectClient +from aiohttp import ClientError +from aiohttp.client_exceptions import ClientConnectionError import voluptuous as vol from homeassistant import config_entries @@ -13,6 +16,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -33,8 +37,11 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ - acc = AladdinConnectClient(data[CONF_USERNAME], data[CONF_PASSWORD]) - login = await hass.async_add_executor_job(acc.login) + acc = AladdinConnectClient( + data[CONF_USERNAME], data[CONF_PASSWORD], async_get_clientsession(hass) + ) + login = await acc.login() + await acc.close() if not login: raise InvalidAuth @@ -67,8 +74,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await validate_input(self.hass, data) + except InvalidAuth: errors["base"] = "invalid_auth" + + except (ClientConnectionError, asyncio.TimeoutError, ClientError): + errors["base"] = "cannot_connect" + else: self.hass.config_entries.async_update_entry( @@ -103,6 +115,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except InvalidAuth: errors["base"] = "invalid_auth" + except (ClientConnectionError, asyncio.TimeoutError, ClientError): + errors["base"] = "cannot_connect" + else: await self.async_set_unique_id( user_input["username"].lower(), raise_on_progress=False diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index da3e6b81663..9c03cd322b6 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -1,10 +1,11 @@ """Platform for the Aladdin Connect cover component.""" from __future__ import annotations +from datetime import timedelta import logging from typing import Any, Final -from aladdin_connect import AladdinConnectClient +from AIOAladdinConnect import AladdinConnectClient import voluptuous as vol from homeassistant.components.cover import ( @@ -34,6 +35,7 @@ _LOGGER: Final = logging.getLogger(__name__) PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} ) +SCAN_INTERVAL = timedelta(seconds=300) async def async_setup_platform( @@ -62,14 +64,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Aladdin Connect platform.""" - acc = hass.data[DOMAIN][config_entry.entry_id] - doors = await hass.async_add_executor_job(acc.get_doors) - + acc: AladdinConnectClient = hass.data[DOMAIN][config_entry.entry_id] + doors = await acc.get_doors() if doors is None: raise PlatformNotReady("Error from Aladdin Connect getting doors") async_add_entities( - (AladdinDevice(acc, door) for door in doors), - update_before_add=True, + (AladdinDevice(acc, door, config_entry) for door in doors), ) @@ -79,27 +79,63 @@ class AladdinDevice(CoverEntity): _attr_device_class = CoverDeviceClass.GARAGE _attr_supported_features = SUPPORTED_FEATURES - def __init__(self, acc: AladdinConnectClient, device: DoorDevice) -> None: + def __init__( + self, acc: AladdinConnectClient, device: DoorDevice, entry: ConfigEntry + ) -> None: """Initialize the Aladdin Connect cover.""" self._acc = acc + self._device_id = device["device_id"] self._number = device["door_number"] self._attr_name = device["name"] self._attr_unique_id = f"{self._device_id}-{self._number}" - def close_cover(self, **kwargs: Any) -> None: + async def async_added_to_hass(self) -> None: + """Connect Aladdin Connect to the cloud.""" + + async def update_callback() -> None: + """Schedule a state update.""" + self.async_write_ha_state() + + self._acc.register_callback(update_callback, self._number) + await self._acc.get_doors(self._number) + + async def async_will_remove_from_hass(self) -> None: + """Close Aladdin Connect before removing.""" + await self._acc.close() + + async def async_close_cover(self, **kwargs: Any) -> None: """Issue close command to cover.""" - self._acc.close_door(self._device_id, self._number) + await self._acc.close_door(self._device_id, self._number) - def open_cover(self, **kwargs: Any) -> None: + async def async_open_cover(self, **kwargs: Any) -> None: """Issue open command to cover.""" - self._acc.open_door(self._device_id, self._number) + await self._acc.open_door(self._device_id, self._number) - def update(self) -> None: + async def async_update(self) -> None: """Update status of cover.""" - status = STATES_MAP.get( - self._acc.get_door_status(self._device_id, self._number) + await self._acc.get_doors(self._number) + + @property + def is_closed(self) -> bool | None: + """Update is closed attribute.""" + value = STATES_MAP.get(self._acc.get_door_status(self._device_id, self._number)) + if value is None: + return None + return value == STATE_CLOSED + + @property + def is_closing(self) -> bool: + """Update is closing attribute.""" + return ( + STATES_MAP.get(self._acc.get_door_status(self._device_id, self._number)) + == STATE_CLOSING + ) + + @property + def is_opening(self) -> bool: + """Update is opening attribute.""" + return ( + STATES_MAP.get(self._acc.get_door_status(self._device_id, self._number)) + == STATE_OPENING ) - self._attr_is_opening = status == STATE_OPENING - self._attr_is_closing = status == STATE_CLOSING - self._attr_is_closed = None if status is None else status == STATE_CLOSED diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index b9ea214d996..3a9e295a08f 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["aladdin_connect==0.4"], + "requirements": ["AIOAladdinConnect==0.1.21"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index a30fd52a2a1..f8aad0172a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,6 +4,9 @@ # homeassistant.components.aemet AEMET-OpenData==0.2.1 +# homeassistant.components.aladdin_connect +AIOAladdinConnect==0.1.21 + # homeassistant.components.adax Adax-local==0.1.4 @@ -282,9 +285,6 @@ airthings_cloud==0.1.0 # homeassistant.components.airtouch4 airtouch4pyapi==1.0.5 -# homeassistant.components.aladdin_connect -aladdin_connect==0.4 - # homeassistant.components.alpha_vantage alpha_vantage==2.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3be3fc1fda1..cb3fa2af723 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -6,6 +6,9 @@ # homeassistant.components.aemet AEMET-OpenData==0.2.1 +# homeassistant.components.aladdin_connect +AIOAladdinConnect==0.1.21 + # homeassistant.components.adax Adax-local==0.1.4 @@ -251,9 +254,6 @@ airthings_cloud==0.1.0 # homeassistant.components.airtouch4 airtouch4pyapi==1.0.5 -# homeassistant.components.aladdin_connect -aladdin_connect==0.4 - # homeassistant.components.ambee ambee==0.4.0 diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py new file mode 100644 index 00000000000..ee68d207361 --- /dev/null +++ b/tests/components/aladdin_connect/conftest.py @@ -0,0 +1,39 @@ +"""Fixtures for the Aladdin Connect integration tests.""" +from unittest import mock +from unittest.mock import AsyncMock + +import pytest + +DEVICE_CONFIG_OPEN = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "open", + "link_status": "Connected", +} + + +@pytest.fixture(name="mock_aladdinconnect_api") +def fixture_mock_aladdinconnect_api(): + """Set up aladdin connect API fixture.""" + with mock.patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient" + ) as mock_opener: + mock_opener.login = AsyncMock(return_value=True) + mock_opener.close = AsyncMock(return_value=True) + + mock_opener.async_get_door_status = AsyncMock(return_value="open") + mock_opener.get_door_status.return_value = "open" + mock_opener.async_get_door_link_status = AsyncMock(return_value="connected") + mock_opener.get_door_link_status.return_value = "connected" + mock_opener.async_get_battery_status = AsyncMock(return_value="99") + mock_opener.get_battery_status.return_value = "99" + mock_opener.async_get_rssi_status = AsyncMock(return_value="-55") + mock_opener.get_rssi_status.return_value = "-55" + mock_opener.get_doors = AsyncMock(return_value=[DEVICE_CONFIG_OPEN]) + + mock_opener.register_callback = mock.Mock(return_value=True) + mock_opener.open_door = AsyncMock(return_value=True) + mock_opener.close_door = AsyncMock(return_value=True) + + yield mock_opener diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 899aa0a7e55..33117c64110 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -1,5 +1,7 @@ """Test the Aladdin Connect config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch + +from aiohttp.client_exceptions import ClientConnectionError from homeassistant import config_entries from homeassistant.components.aladdin_connect.const import DOMAIN @@ -14,8 +16,9 @@ from homeassistant.data_entry_flow import ( from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant) -> None: +async def test_form(hass: HomeAssistant, mock_aladdinconnect_api: MagicMock) -> None: """Test we get the form.""" + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -23,11 +26,10 @@ async def test_form(hass: HomeAssistant) -> None: assert result["errors"] is None with patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - return_value=True, + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ), patch( - "homeassistant.components.aladdin_connect.async_setup_entry", - return_value=True, + "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -44,33 +46,21 @@ async def test_form(hass: HomeAssistant) -> None: CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password", } + assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_failed_auth(hass: HomeAssistant) -> None: +async def test_form_failed_auth( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: """Test we handle failed authentication error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - + mock_aladdinconnect_api.login.return_value = False with patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - return_value=False, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) - - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "invalid_auth"} - - with patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - return_value=False, + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -84,7 +74,33 @@ async def test_form_failed_auth(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_already_configured(hass): +async def test_form_connection_timeout( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: + """Test we handle http timeout error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mock_aladdinconnect_api.login.side_effect = ClientConnectionError + with patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_already_configured( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +): """Test we handle already configured error.""" mock_entry = MockConfigEntry( domain=DOMAIN, @@ -101,8 +117,8 @@ async def test_form_already_configured(hass): assert result["step_id"] == config_entries.SOURCE_USER with patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - return_value=True, + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -117,18 +133,15 @@ async def test_form_already_configured(hass): assert result2["reason"] == "already_configured" -async def test_import_flow_success(hass: HomeAssistant) -> None: +async def test_import_flow_success( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: """Test a successful import of yaml.""" - with patch( - "homeassistant.components.aladdin_connect.cover.async_setup_platform", - return_value=True, + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ), patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.async_setup_entry", - return_value=True, + "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_init( DOMAIN, @@ -149,7 +162,9 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_reauth_flow(hass: HomeAssistant) -> None: +async def test_reauth_flow( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: """Test a successful reauth flow.""" mock_entry = MockConfigEntry( @@ -174,14 +189,11 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["errors"] == {} with patch( - "homeassistant.components.aladdin_connect.cover.async_setup_platform", + "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True, ), patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.async_setup_entry", - return_value=True, + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -197,7 +209,9 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: } -async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: +async def test_reauth_flow_auth_error( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: """Test an authorization error reauth flow.""" mock_entry = MockConfigEntry( @@ -220,13 +234,13 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: assert result["step_id"] == "reauth_confirm" assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {} - + mock_aladdinconnect_api.login.return_value = False with patch( - "homeassistant.components.aladdin_connect.cover.async_setup_platform", - return_value=True, + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ), patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - return_value=False, + "homeassistant.components.aladdin_connect.cover.async_setup_entry", + return_value=True, ), patch( "homeassistant.components.aladdin_connect.cover.async_setup_entry", return_value=True, @@ -239,3 +253,44 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_reauth_flow_connnection_error( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: + """Test a connection error reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-username", "password": "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={"username": "test-username", "password": "new-password"}, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + mock_aladdinconnect_api.login.side_effect = ClientConnectionError + + with patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py index c1571ed9fa2..54ec4ee5de1 100644 --- a/tests/components/aladdin_connect/test_cover.py +++ b/tests/components/aladdin_connect/test_cover.py @@ -1,23 +1,29 @@ """Test the Aladdin Connect Cover.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.components.aladdin_connect.const import DOMAIN +from homeassistant.components.aladdin_connect.cover import SCAN_INTERVAL from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_PASSWORD, CONF_USERNAME, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed YAML_CONFIG = {"username": "test-user", "password": "test-password"} @@ -76,63 +82,11 @@ DEVICE_CONFIG_BAD_NO_DOOR = { } -async def test_setup_get_doors_errors(hass: HomeAssistant) -> None: - """Test component setup Get Doors Errors.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=YAML_CONFIG, - unique_id="test-id", - ) - config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=None, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) is True - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - - -async def test_setup_login_error(hass: HomeAssistant) -> None: - """Test component setup Login Errors.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=YAML_CONFIG, - unique_id="test-id", - ) - config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=False, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) is False - - -async def test_setup_component_noerror(hass: HomeAssistant) -> None: - """Test component setup No Error.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=YAML_CONFIG, - unique_id="test-id", - ) - config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=True, - ): - - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state == ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - -async def test_cover_operation(hass: HomeAssistant) -> None: - """Test component setup open cover, close cover.""" +async def test_cover_operation( + hass: HomeAssistant, + mock_aladdinconnect_api: MagicMock, +) -> None: + """Test Cover Operation states (open,close,opening,closing) cover.""" config_entry = MockConfigEntry( domain=DOMAIN, data=YAML_CONFIG, @@ -142,92 +96,116 @@ async def test_cover_operation(hass: HomeAssistant) -> None: assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() - + mock_aladdinconnect_api.async_get_door_status = AsyncMock(return_value=STATE_OPEN) + mock_aladdinconnect_api.get_door_status.return_value = STATE_OPEN with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_OPEN], + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): - assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert COVER_DOMAIN in hass.config.components - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.open_door", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_OPEN], - ): - await hass.services.async_call( - "cover", "open_cover", {"entity_id": "cover.home"}, blocking=True - ) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.home"}, + blocking=True, + ) + await hass.async_block_till_done() assert hass.states.get("cover.home").state == STATE_OPEN + mock_aladdinconnect_api.async_get_door_status = AsyncMock(return_value=STATE_CLOSED) + mock_aladdinconnect_api.get_door_status.return_value = STATE_CLOSED with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.close_door", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_CLOSED], + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): + await hass.services.async_call( - "cover", "close_cover", {"entity_id": "cover.home"}, blocking=True + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.home"}, + blocking=True, ) + await hass.async_block_till_done() + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + assert hass.states.get("cover.home").state == STATE_CLOSED - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_OPENING], - ): - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True - ) - assert hass.states.get("cover.home").state == STATE_OPENING + mock_aladdinconnect_api.async_get_door_status = AsyncMock( + return_value=STATE_CLOSING + ) + mock_aladdinconnect_api.get_door_status.return_value = STATE_CLOSING with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_CLOSING], + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, ) + await hass.async_block_till_done() assert hass.states.get("cover.home").state == STATE_CLOSING - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_BAD], - ): - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True - ) - assert hass.states.get("cover.home").state + mock_aladdinconnect_api.async_get_door_status = AsyncMock( + return_value=STATE_OPENING + ) + mock_aladdinconnect_api.get_door_status.return_value = STATE_OPENING with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_BAD_NO_DOOR], + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, ) - assert hass.states.get("cover.home").state + await hass.async_block_till_done() + assert hass.states.get("cover.home").state == STATE_OPENING + + mock_aladdinconnect_api.async_get_door_status = AsyncMock(return_value=None) + mock_aladdinconnect_api.get_door_status.return_value = None + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.home"}, + blocking=True, + ) + await hass.async_block_till_done() + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + assert hass.states.get("cover.home").state == STATE_UNKNOWN -async def test_yaml_import(hass: HomeAssistant, caplog: pytest.LogCaptureFixture): +async def test_yaml_import( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mock_aladdinconnect_api: MagicMock, +): """Test setup YAML import.""" assert COVER_DOMAIN not in hass.config.components with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_CLOSED], + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): await async_setup_component( hass, @@ -248,3 +226,37 @@ async def test_yaml_import(hass: HomeAssistant, caplog: pytest.LogCaptureFixture config_data = hass.config_entries.async_entries(DOMAIN)[0].data assert config_data[CONF_USERNAME] == "test-user" assert config_data[CONF_PASSWORD] == "test-password" + + +async def test_callback( + hass: HomeAssistant, + mock_aladdinconnect_api: MagicMock, +): + """Test callback from Aladdin Connect API.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + mock_aladdinconnect_api.async_get_door_status.return_value = STATE_CLOSING + mock_aladdinconnect_api.get_door_status.return_value = STATE_CLOSING + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ), patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient._call_back", + AsyncMock(), + ): + callback = mock_aladdinconnect_api.register_callback.call_args[0][0] + await callback() + assert hass.states.get("cover.home").state == STATE_CLOSING diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py index 0ba9b317dfb..4c422ae29ba 100644 --- a/tests/components/aladdin_connect/test_init.py +++ b/tests/components/aladdin_connect/test_init.py @@ -1,35 +1,77 @@ """Test for Aladdin Connect init logic.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch + +from aiohttp import ClientConnectionError from homeassistant.components.aladdin_connect.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import AsyncMock, MockConfigEntry YAML_CONFIG = {"username": "test-user", "password": "test-password"} -async def test_entry_password_fail(hass: HomeAssistant): - """Test password fail during entry.""" - entry = MockConfigEntry( +async def test_setup_get_doors_errors(hass: HomeAssistant) -> None: + """Test component setup Get Doors Errors.""" + config_entry = MockConfigEntry( domain=DOMAIN, - data={"username": "test-user", "password": "test-password"}, + data=YAML_CONFIG, + unique_id="test-id", ) - entry.add_to_hass(hass) - + config_entry.add_to_hass(hass) with patch( "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=False, + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=None, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + +async def test_setup_login_error( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: + """Test component setup Login Errors.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + mock_aladdinconnect_api.login.return_value = False + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert entry.state is ConfigEntryState.SETUP_ERROR + assert await hass.config_entries.async_setup(config_entry.entry_id) is False -async def test_load_and_unload(hass: HomeAssistant) -> None: - """Test loading and unloading Aladdin Connect entry.""" +async def test_setup_connection_error( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: + """Test component setup Login Errors.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + mock_aladdinconnect_api.login.side_effect = ClientConnectionError + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + + assert await hass.config_entries.async_setup(config_entry.entry_id) is False + + +async def test_setup_component_no_error(hass: HomeAssistant) -> None: + """Test component setup No Error.""" config_entry = MockConfigEntry( domain=DOMAIN, data=YAML_CONFIG, @@ -41,6 +83,49 @@ async def test_load_and_unload(hass: HomeAssistant) -> None: return_value=True, ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +async def test_entry_password_fail( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +): + """Test password fail during entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-user", "password": "test-password"}, + ) + entry.add_to_hass(hass) + mock_aladdinconnect_api.login = AsyncMock(return_value=False) + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_load_and_unload( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: + """Test loading and unloading Aladdin Connect entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done()