diff --git a/.coveragerc b/.coveragerc index 4e78ea6a3e4..7594d2d2d98 100644 --- a/.coveragerc +++ b/.coveragerc @@ -58,6 +58,12 @@ omit = homeassistant/components/airvisual/sensor.py homeassistant/components/airvisual_pro/__init__.py homeassistant/components/airvisual_pro/sensor.py + homeassistant/components/aladdin_connect/__init__.py + homeassistant/components/aladdin_connect/api.py + homeassistant/components/aladdin_connect/application_credentials.py + homeassistant/components/aladdin_connect/cover.py + homeassistant/components/aladdin_connect/model.py + homeassistant/components/aladdin_connect/sensor.py homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/alarm_control_panel.py homeassistant/components/alarmdecoder/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index ddd1e424397..32f885f6015 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -80,8 +80,8 @@ build.json @home-assistant/supervisor /tests/components/airzone/ @Noltari /homeassistant/components/airzone_cloud/ @Noltari /tests/components/airzone_cloud/ @Noltari -/homeassistant/components/aladdin_connect/ @mkmer -/tests/components/aladdin_connect/ @mkmer +/homeassistant/components/aladdin_connect/ @swcloudgenie +/tests/components/aladdin_connect/ @swcloudgenie /homeassistant/components/alarm_control_panel/ @home-assistant/core /tests/components/alarm_control_panel/ @home-assistant/core /homeassistant/components/alert/ @home-assistant/core @frenck diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 84710c3f74e..55c4345beb3 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -1,40 +1,33 @@ -"""The aladdin_connect component.""" +"""The Aladdin Connect Genie integration.""" -import logging -from typing import Final - -from AIOAladdinConnect import AladdinConnectClient -import AIOAladdinConnect.session_manager as Aladdin -from aiohttp import ClientError +from __future__ import annotations from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow -from .const import CLIENT_ID, DOMAIN +from . import api +from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION -_LOGGER: Final = logging.getLogger(__name__) - -PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.COVER] 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, async_get_clientsession(hass), CLIENT_ID + """Set up Aladdin Connect Genie from a config entry.""" + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + + # If using an aiohttp-based API lib + entry.runtime_data = api.AsyncConfigEntryAuth( + aiohttp_client.async_get_clientsession(hass), session ) - try: - await acc.login() - except (ClientError, TimeoutError, Aladdin.ConnectionError) as ex: - raise ConfigEntryNotReady("Can not connect to host") from ex - except Aladdin.InvalidPasswordError as ex: - raise ConfigEntryAuthFailed("Incorrect Password") from ex - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -42,7 +35,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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 await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - return unload_ok + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old config.""" + if config_entry.version < CONFIG_FLOW_VERSION: + config_entry.async_start_reauth(hass) + new_data = {**config_entry.data} + hass.config_entries.async_update_entry( + config_entry, + data=new_data, + version=CONFIG_FLOW_VERSION, + minor_version=CONFIG_FLOW_MINOR_VERSION, + ) + + return True diff --git a/homeassistant/components/aladdin_connect/api.py b/homeassistant/components/aladdin_connect/api.py new file mode 100644 index 00000000000..8100cd1e4d8 --- /dev/null +++ b/homeassistant/components/aladdin_connect/api.py @@ -0,0 +1,31 @@ +"""API for Aladdin Connect Genie bound to Home Assistant OAuth.""" + +from aiohttp import ClientSession +from genie_partner_sdk.auth import Auth + +from homeassistant.helpers import config_entry_oauth2_flow + +API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1" +API_KEY = "k6QaiQmcTm2zfaNns5L1Z8duBtJmhDOW8JawlCC3" + + +class AsyncConfigEntryAuth(Auth): # type: ignore[misc] + """Provide Aladdin Connect Genie authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + websession: ClientSession, + oauth_session: config_entry_oauth2_flow.OAuth2Session, + ) -> None: + """Initialize Aladdin Connect Genie auth.""" + super().__init__( + websession, API_URL, oauth_session.token["access_token"], API_KEY + ) + self._oauth_session = oauth_session + + async def async_get_access_token(self) -> str: + """Return a valid access token.""" + if not self._oauth_session.valid_token: + await self._oauth_session.async_ensure_token_valid() + + return str(self._oauth_session.token["access_token"]) diff --git a/homeassistant/components/aladdin_connect/application_credentials.py b/homeassistant/components/aladdin_connect/application_credentials.py new file mode 100644 index 00000000000..e8e959f1fa3 --- /dev/null +++ b/homeassistant/components/aladdin_connect/application_credentials.py @@ -0,0 +1,14 @@ +"""application_credentials platform the Aladdin Connect Genie integration.""" + +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + +from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + authorize_url=OAUTH2_AUTHORIZE, + token_url=OAUTH2_TOKEN, + ) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index e960138853a..aa42574a005 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -1,137 +1,58 @@ -"""Config flow for Aladdin Connect cover integration.""" - -from __future__ import annotations +"""Config flow for Aladdin Connect Genie.""" from collections.abc import Mapping +import logging from typing import Any -from AIOAladdinConnect import AladdinConnectClient -import AIOAladdinConnect.session_manager as Aladdin -from aiohttp.client_exceptions import ClientError import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult +from homeassistant.helpers import config_entry_oauth2_flow -from .const import CLIENT_ID, DOMAIN - -STEP_USER_DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - } -) - -REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) +from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: - """Validate the user input allows us to connect. +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Aladdin Connect Genie OAuth2 authentication.""" - Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. - """ - acc = AladdinConnectClient( - data[CONF_USERNAME], - data[CONF_PASSWORD], - async_get_clientsession(hass), - CLIENT_ID, - ) - try: - await acc.login() - except (ClientError, TimeoutError, Aladdin.ConnectionError): - raise + DOMAIN = DOMAIN + VERSION = CONFIG_FLOW_VERSION + MINOR_VERSION = CONFIG_FLOW_MINOR_VERSION - except Aladdin.InvalidPasswordError as ex: - raise InvalidAuth from ex - - -class AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Aladdin Connect.""" - - VERSION = 1 - entry: ConfigEntry | None + reauth_entry: ConfigEntry | None = None async def async_step_reauth( - self, entry_data: Mapping[str, Any] + self, user_input: Mapping[str, Any] ) -> ConfigFlowResult: - """Handle re-authentication with Aladdin Connect.""" - - self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + """Perform reauth upon API auth error or upgrade from v1 to v2.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( - self, user_input: dict[str, Any] | None = None + self, user_input: Mapping[str, Any] | None = None ) -> ConfigFlowResult: - """Confirm re-authentication with Aladdin Connect.""" - errors: dict[str, str] = {} - - if user_input: - assert self.entry is not None - password = user_input[CONF_PASSWORD] - data = { - CONF_USERNAME: self.entry.data[CONF_USERNAME], - CONF_PASSWORD: password, - } - - try: - await validate_input(self.hass, data) - - except InvalidAuth: - errors["base"] = "invalid_auth" - - except (ClientError, TimeoutError, Aladdin.ConnectionError): - errors["base"] = "cannot_connect" - - else: - self.hass.config_entries.async_update_entry( - self.entry, - data={ - **self.entry.data, - CONF_PASSWORD: password, - }, - ) - await self.hass.config_entries.async_reload(self.entry.entry_id) - return self.async_abort(reason="reauth_successful") - - return self.async_show_form( - step_id="reauth_confirm", - data_schema=REAUTH_SCHEMA, - errors=errors, - ) - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle the initial step.""" + """Dialog that informs the user that reauth is required.""" if user_input is None: return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA + step_id="reauth_confirm", + data_schema=vol.Schema({}), ) + return await self.async_step_user() - errors = {} - - try: - await validate_input(self.hass, user_input) - except InvalidAuth: - errors["base"] = "invalid_auth" - - except (ClientError, TimeoutError, Aladdin.ConnectionError): - errors["base"] = "cannot_connect" - - else: - await self.async_set_unique_id( - user_input["username"].lower(), raise_on_progress=False + async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult: + """Create an oauth config entry or update existing entry for reauth.""" + if self.reauth_entry: + return self.async_update_reload_and_abort( + self.reauth_entry, + data=data, ) - self._abort_if_unique_id_configured() - return self.async_create_entry(title="Aladdin Connect", data=user_input) + return await super().async_oauth_create_entry(data) - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors - ) - - -class InvalidAuth(HomeAssistantError): - """Error to indicate there is invalid auth.""" + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py index bf77c032d1b..5312826469e 100644 --- a/homeassistant/components/aladdin_connect/const.py +++ b/homeassistant/components/aladdin_connect/const.py @@ -1,22 +1,14 @@ -"""Platform for the Aladdin Connect cover component.""" - -from __future__ import annotations +"""Constants for the Aladdin Connect Genie integration.""" from typing import Final from homeassistant.components.cover import CoverEntityFeature -from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING - -NOTIFICATION_ID: Final = "aladdin_notification" -NOTIFICATION_TITLE: Final = "Aladdin Connect Cover Setup" - -STATES_MAP: Final[dict[str, str]] = { - "open": STATE_OPEN, - "opening": STATE_OPENING, - "closed": STATE_CLOSED, - "closing": STATE_CLOSING, -} DOMAIN = "aladdin_connect" +CONFIG_FLOW_VERSION = 2 +CONFIG_FLOW_MINOR_VERSION = 1 + +OAUTH2_AUTHORIZE = "https://app.aladdinconnect.com/login.html" +OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token" + SUPPORTED_FEATURES: Final = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE -CLIENT_ID = "1000" diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 61c8df92eaf..cf31b06cbcd 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -1,25 +1,23 @@ -"""Platform for the Aladdin Connect cover component.""" - -from __future__ import annotations +"""Cover Entity for Genie Garage Door.""" from datetime import timedelta from typing import Any -from AIOAladdinConnect import AladdinConnectClient, session_manager +from genie_partner_sdk.client import AladdinConnectClient from homeassistant.components.cover import CoverDeviceClass, CoverEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, PlatformNotReady +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, STATES_MAP, SUPPORTED_FEATURES -from .model import DoorDevice +from . import api +from .const import DOMAIN, SUPPORTED_FEATURES +from .model import GarageDoor -SCAN_INTERVAL = timedelta(seconds=300) +SCAN_INTERVAL = timedelta(seconds=15) async def async_setup_entry( @@ -28,25 +26,33 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Aladdin Connect platform.""" - acc: AladdinConnectClient = hass.data[DOMAIN][config_entry.entry_id] + session: api.AsyncConfigEntryAuth = config_entry.runtime_data + acc = AladdinConnectClient(session) doors = await acc.get_doors() if doors is None: raise PlatformNotReady("Error from Aladdin Connect getting doors") + device_registry = dr.async_get(hass) + doors_to_add = [] + for door in doors: + existing = device_registry.async_get(door.unique_id) + if existing is None: + doors_to_add.append(door) + async_add_entities( - (AladdinDevice(acc, door, config_entry) for door in doors), + (AladdinDevice(acc, door, config_entry) for door in doors_to_add), ) remove_stale_devices(hass, config_entry, doors) def remove_stale_devices( - hass: HomeAssistant, config_entry: ConfigEntry, devices: list[dict] + hass: HomeAssistant, config_entry: ConfigEntry, devices: list[GarageDoor] ) -> None: """Remove stale devices from device registry.""" device_registry = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id ) - all_device_ids = {f"{door['device_id']}-{door['door_number']}" for door in devices} + all_device_ids = {door.unique_id for door in devices} for device_entry in device_entries: device_id: str | None = None @@ -74,74 +80,52 @@ class AladdinDevice(CoverEntity): _attr_name = None def __init__( - self, acc: AladdinConnectClient, device: DoorDevice, entry: ConfigEntry + self, acc: AladdinConnectClient, device: GarageDoor, entry: ConfigEntry ) -> None: """Initialize the Aladdin Connect cover.""" self._acc = acc - self._device_id = device["device_id"] - self._number = device["door_number"] - self._serial = device["serial"] + self._device_id = device.device_id + self._number = device.door_number self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, f"{self._device_id}-{self._number}")}, - name=device["name"], + identifiers={(DOMAIN, device.unique_id)}, + name=device.name, manufacturer="Overhead Door", - model=device["model"], ) - self._attr_unique_id = f"{self._device_id}-{self._number}" - - async def async_added_to_hass(self) -> None: - """Connect Aladdin Connect to the cloud.""" - - self._acc.register_callback( - self.async_write_ha_state, self._serial, self._number - ) - await self._acc.get_doors(self._serial) - - async def async_will_remove_from_hass(self) -> None: - """Close Aladdin Connect before removing.""" - self._acc.unregister_callback(self._serial, self._number) - await self._acc.close() - - async def async_close_cover(self, **kwargs: Any) -> None: - """Issue close command to cover.""" - if not await self._acc.close_door(self._device_id, self._number): - raise HomeAssistantError("Aladdin Connect API failed to close the cover") + self._attr_unique_id = device.unique_id async def async_open_cover(self, **kwargs: Any) -> None: """Issue open command to cover.""" - if not await self._acc.open_door(self._device_id, self._number): - raise HomeAssistantError("Aladdin Connect API failed to open the cover") + await self._acc.open_door(self._device_id, self._number) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Issue close command to cover.""" + await self._acc.close_door(self._device_id, self._number) async def async_update(self) -> None: """Update status of cover.""" - try: - await self._acc.get_doors(self._serial) - self._attr_available = True - - except (session_manager.ConnectionError, session_manager.InvalidPasswordError): - self._attr_available = False + await self._acc.update_door(self._device_id, 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)) + value = self._acc.get_door_status(self._device_id, self._number) if value is None: return None - return value == STATE_CLOSED + return bool(value == "closed") @property - def is_closing(self) -> bool: + def is_closing(self) -> bool | None: """Update is closing attribute.""" - return ( - STATES_MAP.get(self._acc.get_door_status(self._device_id, self._number)) - == STATE_CLOSING - ) + value = self._acc.get_door_status(self._device_id, self._number) + if value is None: + return None + return bool(value == "closing") @property - def is_opening(self) -> bool: + def is_opening(self) -> bool | None: """Update is opening attribute.""" - return ( - STATES_MAP.get(self._acc.get_door_status(self._device_id, self._number)) - == STATE_OPENING - ) + value = self._acc.get_door_status(self._device_id, self._number) + if value is None: + return None + return bool(value == "opening") diff --git a/homeassistant/components/aladdin_connect/diagnostics.py b/homeassistant/components/aladdin_connect/diagnostics.py deleted file mode 100644 index 67a31079f14..00000000000 --- a/homeassistant/components/aladdin_connect/diagnostics.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Diagnostics support for Aladdin Connect.""" - -from __future__ import annotations - -from typing import Any - -from AIOAladdinConnect import AladdinConnectClient - -from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant - -from .const import DOMAIN - -TO_REDACT = {"serial", "device_id"} - - -async def async_get_config_entry_diagnostics( - hass: HomeAssistant, - config_entry: ConfigEntry, -) -> dict[str, Any]: - """Return diagnostics for a config entry.""" - - acc: AladdinConnectClient = hass.data[DOMAIN][config_entry.entry_id] - - return { - "doors": async_redact_data(acc.doors, TO_REDACT), - } diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 344c77dcb73..69b38399cce 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -1,11 +1,10 @@ { "domain": "aladdin_connect", "name": "Aladdin Connect", - "codeowners": ["@mkmer"], + "codeowners": ["@swcloudgenie"], "config_flow": true, + "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "iot_class": "cloud_polling", - "loggers": ["aladdin_connect"], - "quality_scale": "platinum", - "requirements": ["AIOAladdinConnect==0.1.58"] + "requirements": ["genie-partner-sdk==1.0.2"] } diff --git a/homeassistant/components/aladdin_connect/model.py b/homeassistant/components/aladdin_connect/model.py index 73e445f2f3b..db08cb7b8b8 100644 --- a/homeassistant/components/aladdin_connect/model.py +++ b/homeassistant/components/aladdin_connect/model.py @@ -5,12 +5,26 @@ from __future__ import annotations from typing import TypedDict -class DoorDevice(TypedDict): - """Aladdin door device.""" +class GarageDoorData(TypedDict): + """Aladdin door data.""" device_id: str door_number: int name: str status: str - serial: str - model: str + link_status: str + battery_level: int + + +class GarageDoor: + """Aladdin Garage Door Entity.""" + + def __init__(self, data: GarageDoorData) -> None: + """Create `GarageDoor` from dictionary of data.""" + self.device_id = data["device_id"] + self.door_number = data["door_number"] + self.unique_id = f"{self.device_id}-{self.door_number}" + self.name = data["name"] + self.status = data["status"] + self.link_status = data["link_status"] + self.battery_level = data["battery_level"] diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py index 22aa9c6faf0..231928656a8 100644 --- a/homeassistant/components/aladdin_connect/sensor.py +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -6,7 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import cast -from AIOAladdinConnect import AladdinConnectClient +from genie_partner_sdk.client import AladdinConnectClient from homeassistant.components.sensor import ( SensorDeviceClass, @@ -15,13 +15,14 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import api from .const import DOMAIN -from .model import DoorDevice +from .model import GarageDoor @dataclass(frozen=True, kw_only=True) @@ -40,24 +41,6 @@ SENSORS: tuple[AccSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=AladdinConnectClient.get_battery_status, ), - AccSensorEntityDescription( - key="rssi", - translation_key="wifi_strength", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_registry_enabled_default=False, - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, - state_class=SensorStateClass.MEASUREMENT, - value_fn=AladdinConnectClient.get_rssi_status, - ), - AccSensorEntityDescription( - key="ble_strength", - translation_key="ble_strength", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_registry_enabled_default=False, - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, - state_class=SensorStateClass.MEASUREMENT, - value_fn=AladdinConnectClient.get_ble_strength, - ), ) @@ -66,7 +49,8 @@ async def async_setup_entry( ) -> None: """Set up Aladdin Connect sensor devices.""" - acc: AladdinConnectClient = hass.data[DOMAIN][entry.entry_id] + session: api.AsyncConfigEntryAuth = hass.data[DOMAIN][entry.entry_id] + acc = AladdinConnectClient(session) entities = [] doors = await acc.get_doors() @@ -88,26 +72,20 @@ class AladdinConnectSensor(SensorEntity): def __init__( self, acc: AladdinConnectClient, - device: DoorDevice, + device: GarageDoor, description: AccSensorEntityDescription, ) -> None: """Initialize a sensor for an Aladdin Connect device.""" - self._device_id = device["device_id"] - self._number = device["door_number"] + self._device_id = device.device_id + self._number = device.door_number self._acc = acc self.entity_description = description - self._attr_unique_id = f"{self._device_id}-{self._number}-{description.key}" + self._attr_unique_id = f"{device.unique_id}-{description.key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, f"{self._device_id}-{self._number}")}, - name=device["name"], + identifiers={(DOMAIN, device.unique_id)}, + name=device.name, manufacturer="Overhead Door", - model=device["model"], ) - if device["model"] == "01" and description.key in ( - "battery_level", - "ble_strength", - ): - self._attr_entity_registry_enabled_default = True @property def native_value(self) -> float | None: diff --git a/homeassistant/components/aladdin_connect/strings.json b/homeassistant/components/aladdin_connect/strings.json index bfe932b039c..48f9b299a1d 100644 --- a/homeassistant/components/aladdin_connect/strings.json +++ b/homeassistant/components/aladdin_connect/strings.json @@ -1,39 +1,29 @@ { "config": { "step": { - "user": { - "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" - } + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "The Aladdin Connect integration needs to re-authenticate your account", - "data": { - "password": "[%key:common::config_flow::data::password%]" - } + "description": "Aladdin Connect needs to re-authenticate your account" } }, - - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - } - }, - "entity": { - "sensor": { - "wifi_strength": { - "name": "Wi-Fi RSSI" - }, - "ble_strength": { - "name": "BLE Strength" - } + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" } } } diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index c576f242e30..bc6b29e4c23 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -4,6 +4,7 @@ To update, run python3 -m script.hassfest """ APPLICATION_CREDENTIALS = [ + "aladdin_connect", "electric_kiwi", "fitbit", "geocaching", diff --git a/requirements_all.txt b/requirements_all.txt index 3d297241539..c7ee7ae5623 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,9 +6,6 @@ # homeassistant.components.aemet AEMET-OpenData==0.5.1 -# homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.58 - # homeassistant.components.honeywell AIOSomecomfort==0.0.25 @@ -923,6 +920,9 @@ gassist-text==0.0.11 # homeassistant.components.google gcal-sync==6.0.4 +# homeassistant.components.aladdin_connect +genie-partner-sdk==1.0.2 + # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index faeb0bdfcdb..ccc1ae213ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -6,9 +6,6 @@ # homeassistant.components.aemet AEMET-OpenData==0.5.1 -# homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.58 - # homeassistant.components.honeywell AIOSomecomfort==0.0.25 @@ -758,6 +755,9 @@ gassist-text==0.0.11 # homeassistant.components.google gcal-sync==6.0.4 +# homeassistant.components.aladdin_connect +genie-partner-sdk==1.0.2 + # homeassistant.components.geocaching geocachingapi==0.2.1 diff --git a/tests/components/aladdin_connect/__init__.py b/tests/components/aladdin_connect/__init__.py index 6e108ed88df..aa5957dc392 100644 --- a/tests/components/aladdin_connect/__init__.py +++ b/tests/components/aladdin_connect/__init__.py @@ -1 +1 @@ -"""The tests for Aladdin Connect platforms.""" +"""Tests for the Aladdin Connect Garage Door integration.""" diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py deleted file mode 100644 index 979c30bdcea..00000000000 --- a/tests/components/aladdin_connect/conftest.py +++ /dev/null @@ -1,48 +0,0 @@ -"""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", - "serial": "12345", - "model": "02", - "rssi": -67, - "ble_strength": 0, - "vendor": "GENIE", - "battery_level": 0, -} - - -@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.async_get_ble_strength = AsyncMock(return_value="-45") - mock_opener.get_ble_strength.return_value = "-45" - mock_opener.get_doors = AsyncMock(return_value=[DEVICE_CONFIG_OPEN]) - mock_opener.doors = [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) - - return mock_opener diff --git a/tests/components/aladdin_connect/snapshots/test_diagnostics.ambr b/tests/components/aladdin_connect/snapshots/test_diagnostics.ambr deleted file mode 100644 index 8f96567a49f..00000000000 --- a/tests/components/aladdin_connect/snapshots/test_diagnostics.ambr +++ /dev/null @@ -1,20 +0,0 @@ -# serializer version: 1 -# name: test_entry_diagnostics - dict({ - 'doors': list([ - dict({ - 'battery_level': 0, - 'ble_strength': 0, - 'device_id': '**REDACTED**', - 'door_number': 1, - 'link_status': 'Connected', - 'model': '02', - 'name': 'home', - 'rssi': -67, - 'serial': '**REDACTED**', - 'status': 'open', - 'vendor': 'GENIE', - }), - ]), - }) -# --- diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 65b8b24a59d..d460d62625b 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -1,278 +1,82 @@ -"""Test the Aladdin Connect config flow.""" +"""Test the Aladdin Connect Garage Door config flow.""" -from unittest.mock import MagicMock, patch +from unittest.mock import patch -from AIOAladdinConnect.session_manager import InvalidPasswordError -from aiohttp.client_exceptions import ClientConnectionError +import pytest from homeassistant import config_entries -from homeassistant.components.aladdin_connect.const import DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.aladdin_connect.const import ( + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, +) +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" -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} - ) - assert result["type"] is FlowResultType.FORM - assert result["errors"] is None - - with ( - patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ), - patch( - "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"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "Aladdin Connect" - assert result2["data"] == { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - } - - assert len(mock_setup_entry.mock_calls) == 1 - - -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 - mock_aladdinconnect_api.login.side_effect = InvalidPasswordError - 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"] is FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_auth"} - - -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"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_already_configured( - hass: HomeAssistant, mock_aladdinconnect_api: MagicMock -) -> None: - """Test we handle already configured error.""" - mock_entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_USERNAME: "test-username", CONF_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_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == config_entries.SOURCE_USER - - 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", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "already_configured" - - -async def test_reauth_flow( - hass: HomeAssistant, mock_aladdinconnect_api: MagicMock -) -> None: - """Test a successful 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( +@pytest.fixture +async def setup_credentials(hass: HomeAssistant) -> None: + """Fixture to setup credentials.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, 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"}, + ClientCredential(CLIENT_ID, CLIENT_SECRET), ) - assert result["step_id"] == "reauth_confirm" - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} - with ( - patch( - "homeassistant.components.aladdin_connect.async_setup_entry", - return_value=True, - ), - 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"] is FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" - assert mock_entry.data == { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "new-password", - } - - -async def test_reauth_flow_auth_error( - hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +async def test_full_flow( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, + setup_credentials, ) -> None: - """Test an authorization 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) - + """Check full flow.""" 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, + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( # noqa: SLF001 + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", }, - data={"username": "test-username", "password": "new-password"}, ) - assert result["step_id"] == "reauth_confirm" - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} - mock_aladdinconnect_api.login.return_value = False - mock_aladdinconnect_api.login.side_effect = InvalidPasswordError - with ( - patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ), - patch( - "homeassistant.components.aladdin_connect.cover.async_setup_entry", - return_value=True, - ), - patch( - "homeassistant.components.aladdin_connect.cover.async_setup_entry", - return_value=True, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_PASSWORD: "new-password"}, - ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.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", + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" ) - 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, + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, }, - data={"username": "test-username", "password": "new-password"}, ) - assert result["step_id"] == "reauth_confirm" - assert result["type"] is FlowResultType.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() + "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True + ) as mock_setup: + await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py deleted file mode 100644 index 082ade75ab9..00000000000 --- a/tests/components/aladdin_connect/test_cover.py +++ /dev/null @@ -1,228 +0,0 @@ -"""Test the Aladdin Connect Cover.""" - -from unittest.mock import AsyncMock, MagicMock, patch - -from AIOAladdinConnect import session_manager -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, - SERVICE_CLOSE_COVER, - SERVICE_OPEN_COVER, - STATE_CLOSED, - STATE_CLOSING, - STATE_OPEN, - STATE_OPENING, - STATE_UNAVAILABLE, - STATE_UNKNOWN, -) -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow - -from tests.common import MockConfigEntry, async_fire_time_changed - -YAML_CONFIG = {"username": "test-user", "password": "test-password"} - -DEVICE_CONFIG_OPEN = { - "device_id": 533255, - "door_number": 1, - "name": "home", - "status": "open", - "link_status": "Connected", - "serial": "12345", -} - -DEVICE_CONFIG_OPENING = { - "device_id": 533255, - "door_number": 1, - "name": "home", - "status": "opening", - "link_status": "Connected", - "serial": "12345", -} - -DEVICE_CONFIG_CLOSED = { - "device_id": 533255, - "door_number": 1, - "name": "home", - "status": "closed", - "link_status": "Connected", - "serial": "12345", -} - -DEVICE_CONFIG_CLOSING = { - "device_id": 533255, - "door_number": 1, - "name": "home", - "status": "closing", - "link_status": "Connected", - "serial": "12345", -} - -DEVICE_CONFIG_DISCONNECTED = { - "device_id": 533255, - "door_number": 1, - "name": "home", - "status": "open", - "link_status": "Disconnected", - "serial": "12345", -} - -DEVICE_CONFIG_BAD = { - "device_id": 533255, - "door_number": 1, - "name": "home", - "status": "open", -} -DEVICE_CONFIG_BAD_NO_DOOR = { - "device_id": 533255, - "door_number": 2, - "name": "home", - "status": "open", - "link_status": "Disconnected", -} - - -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, - unique_id="test-id", - ) - config_entry.add_to_hass(hass) - - 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.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert COVER_DOMAIN in hass.config.components - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.home"}, - blocking=True, - ) - assert hass.states.get("cover.home").state == STATE_OPEN - - mock_aladdinconnect_api.open_door.return_value = False - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.home"}, - blocking=True, - ) - - mock_aladdinconnect_api.open_door.return_value = True - - mock_aladdinconnect_api.async_get_door_status = AsyncMock(return_value=STATE_CLOSED) - mock_aladdinconnect_api.get_door_status.return_value = STATE_CLOSED - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {ATTR_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_CLOSED - - mock_aladdinconnect_api.close_door.return_value = False - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.home"}, - blocking=True, - ) - - mock_aladdinconnect_api.close_door.return_value = True - - mock_aladdinconnect_api.async_get_door_status = AsyncMock( - return_value=STATE_CLOSING - ) - mock_aladdinconnect_api.get_door_status.return_value = STATE_CLOSING - - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - await hass.async_block_till_done() - assert hass.states.get("cover.home").state == STATE_CLOSING - - mock_aladdinconnect_api.async_get_door_status = AsyncMock( - return_value=STATE_OPENING - ) - mock_aladdinconnect_api.get_door_status.return_value = STATE_OPENING - - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - 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 - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {ATTR_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_UNKNOWN - - mock_aladdinconnect_api.get_doors.side_effect = session_manager.ConnectionError - - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - await hass.async_block_till_done() - - assert hass.states.get("cover.home").state == STATE_UNAVAILABLE - - mock_aladdinconnect_api.get_doors.side_effect = session_manager.InvalidPasswordError - mock_aladdinconnect_api.login.return_value = False - mock_aladdinconnect_api.login.side_effect = session_manager.InvalidPasswordError - - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - await hass.async_block_till_done() - assert hass.states.get("cover.home").state == STATE_UNAVAILABLE diff --git a/tests/components/aladdin_connect/test_diagnostics.py b/tests/components/aladdin_connect/test_diagnostics.py deleted file mode 100644 index 48741c77cd1..00000000000 --- a/tests/components/aladdin_connect/test_diagnostics.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Test AccuWeather diagnostics.""" - -from unittest.mock import MagicMock, patch - -from syrupy import SnapshotAssertion - -from homeassistant.components.aladdin_connect.const import DOMAIN -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry -from tests.components.diagnostics import get_diagnostics_for_config_entry -from tests.typing import ClientSessionGenerator - -YAML_CONFIG = {"username": "test-user", "password": "test-password"} - - -async def test_entry_diagnostics( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - snapshot: SnapshotAssertion, - mock_aladdinconnect_api: MagicMock, -) -> None: - """Test config entry diagnostics.""" - - 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() - - result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - - assert result == snapshot diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py deleted file mode 100644 index bcc32101437..00000000000 --- a/tests/components/aladdin_connect/test_init.py +++ /dev/null @@ -1,258 +0,0 @@ -"""Test for Aladdin Connect init logic.""" - -from unittest.mock import MagicMock, patch - -from AIOAladdinConnect.session_manager import InvalidPasswordError -from aiohttp import ClientConnectionError - -from homeassistant.components.aladdin_connect.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr - -from .conftest import DEVICE_CONFIG_OPEN - -from tests.common import AsyncMock, MockConfigEntry - -CONFIG = {"username": "test-user", "password": "test-password"} -ID = "533255-1" - - -async def test_setup_get_doors_errors(hass: HomeAssistant) -> None: - """Test component setup Get Doors Errors.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=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, mock_aladdinconnect_api: MagicMock -) -> None: - """Test component setup Login Errors.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG, - unique_id=ID, - ) - config_entry.add_to_hass(hass) - mock_aladdinconnect_api.login.return_value = False - mock_aladdinconnect_api.login.side_effect = InvalidPasswordError - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) is False - - -async def test_setup_connection_error( - hass: HomeAssistant, mock_aladdinconnect_api: MagicMock -) -> None: - """Test component setup Login Errors.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG, - unique_id=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=CONFIG, - unique_id=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 is ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - -async def test_entry_password_fail( - hass: HomeAssistant, mock_aladdinconnect_api: MagicMock -) -> None: - """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) - mock_aladdinconnect_api.login.side_effect = InvalidPasswordError - 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=CONFIG, - unique_id=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() - - assert config_entry.state is ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - assert await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.NOT_LOADED - - -async def test_stale_device_removal( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mock_aladdinconnect_api: MagicMock, -) -> None: - """Test component setup missing door device is removed.""" - DEVICE_CONFIG_DOOR_2 = { - "device_id": 533255, - "door_number": 2, - "name": "home 2", - "status": "open", - "link_status": "Connected", - "serial": "12346", - "model": "02", - } - config_entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG, - unique_id=ID, - ) - config_entry.add_to_hass(hass) - mock_aladdinconnect_api.get_doors = AsyncMock( - return_value=[DEVICE_CONFIG_OPEN, DEVICE_CONFIG_DOOR_2] - ) - config_entry_other = MockConfigEntry( - domain="OtherDomain", - data=CONFIG, - unique_id="unique_id", - ) - config_entry_other.add_to_hass(hass) - - device_entry_other = device_registry.async_get_or_create( - config_entry_id=config_entry_other.entry_id, - identifiers={("OtherDomain", "533255-2")}, - ) - device_registry.async_update_device( - device_entry_other.id, - add_config_entry_id=config_entry.entry_id, - merge_identifiers={(DOMAIN, "533255-2")}, - ) - - with patch( - "homeassistant.components.aladdin_connect.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - device_entries = dr.async_entries_for_config_entry( - device_registry, config_entry.entry_id - ) - - assert len(device_entries) == 2 - assert any((DOMAIN, "533255-1") in device.identifiers for device in device_entries) - assert any((DOMAIN, "533255-2") in device.identifiers for device in device_entries) - assert any( - ("OtherDomain", "533255-2") in device.identifiers for device in device_entries - ) - - device_entries_other = dr.async_entries_for_config_entry( - device_registry, config_entry_other.entry_id - ) - assert len(device_entries_other) == 1 - assert any( - (DOMAIN, "533255-2") in device.identifiers for device in device_entries_other - ) - assert any( - ("OtherDomain", "533255-2") in device.identifiers - for device in device_entries_other - ) - - assert await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.NOT_LOADED - - mock_aladdinconnect_api.get_doors = AsyncMock(return_value=[DEVICE_CONFIG_OPEN]) - with patch( - "homeassistant.components.aladdin_connect.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - device_entries = dr.async_entries_for_config_entry( - device_registry, config_entry.entry_id - ) - assert len(device_entries) == 1 - assert any((DOMAIN, "533255-1") in device.identifiers for device in device_entries) - assert not any( - (DOMAIN, "533255-2") in device.identifiers for device in device_entries - ) - assert not any( - ("OtherDomain", "533255-2") in device.identifiers for device in device_entries - ) - - device_entries_other = dr.async_entries_for_config_entry( - device_registry, config_entry_other.entry_id - ) - - assert len(device_entries_other) == 1 - assert any( - ("OtherDomain", "533255-2") in device.identifiers - for device in device_entries_other - ) - assert any( - (DOMAIN, "533255-2") in device.identifiers for device in device_entries_other - ) diff --git a/tests/components/aladdin_connect/test_model.py b/tests/components/aladdin_connect/test_model.py deleted file mode 100644 index 84b1c9ae40a..00000000000 --- a/tests/components/aladdin_connect/test_model.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Test the Aladdin Connect model class.""" - -from homeassistant.components.aladdin_connect.model import DoorDevice -from homeassistant.core import HomeAssistant - - -async def test_model(hass: HomeAssistant) -> None: - """Test model for Aladdin Connect Model.""" - test_values = { - "device_id": "1", - "door_number": "2", - "name": "my door", - "status": "good", - } - result2 = DoorDevice(test_values) - assert result2["device_id"] == "1" - assert result2["door_number"] == "2" - assert result2["name"] == "my door" - assert result2["status"] == "good" diff --git a/tests/components/aladdin_connect/test_sensor.py b/tests/components/aladdin_connect/test_sensor.py deleted file mode 100644 index 9c229e2ac5e..00000000000 --- a/tests/components/aladdin_connect/test_sensor.py +++ /dev/null @@ -1,165 +0,0 @@ -"""Test the Aladdin Connect Sensors.""" - -from datetime import timedelta -from unittest.mock import AsyncMock, MagicMock, patch - -from homeassistant.components.aladdin_connect.const import DOMAIN -from homeassistant.components.aladdin_connect.cover import SCAN_INTERVAL -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -from homeassistant.util.dt import utcnow - -from tests.common import MockConfigEntry, async_fire_time_changed - -DEVICE_CONFIG_MODEL_01 = { - "device_id": 533255, - "door_number": 1, - "name": "home", - "status": "closed", - "link_status": "Connected", - "serial": "12345", - "model": "01", -} - - -CONFIG = {"username": "test-user", "password": "test-password"} -RELOAD_AFTER_UPDATE_DELAY = timedelta(seconds=31) - - -async def test_sensors( - hass: HomeAssistant, - mock_aladdinconnect_api: MagicMock, - entity_registry: er.EntityRegistry, -) -> None: - """Test Sensors for AladdinConnect.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=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() - - entry = entity_registry.async_get("sensor.home_battery") - assert entry - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - update_entry = entity_registry.async_update_entity( - entry.entity_id, disabled_by=None - ) - await hass.async_block_till_done() - assert update_entry != entry - assert update_entry.disabled is False - state = hass.states.get("sensor.home_battery") - assert state is None - - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - await hass.async_block_till_done() - state = hass.states.get("sensor.home_battery") - assert state - - entry = entity_registry.async_get("sensor.home_wi_fi_rssi") - await hass.async_block_till_done() - assert entry - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - update_entry = entity_registry.async_update_entity( - entry.entity_id, disabled_by=None - ) - await hass.async_block_till_done() - assert update_entry != entry - assert update_entry.disabled is False - state = hass.states.get("sensor.home_wi_fi_rssi") - assert state is None - - update_entry = entity_registry.async_update_entity( - entry.entity_id, disabled_by=None - ) - await hass.async_block_till_done() - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - await hass.async_block_till_done() - - state = hass.states.get("sensor.home_wi_fi_rssi") - assert state - - -async def test_sensors_model_01( - hass: HomeAssistant, - mock_aladdinconnect_api: MagicMock, - entity_registry: er.EntityRegistry, -) -> None: - """Test Sensors for AladdinConnect.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=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, - ): - mock_aladdinconnect_api.get_doors = AsyncMock( - return_value=[DEVICE_CONFIG_MODEL_01] - ) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - entry = entity_registry.async_get("sensor.home_battery") - assert entry - assert entry.disabled is False - assert entry.disabled_by is None - state = hass.states.get("sensor.home_battery") - assert state - - entry = entity_registry.async_get("sensor.home_wi_fi_rssi") - await hass.async_block_till_done() - assert entry - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - update_entry = entity_registry.async_update_entity( - entry.entity_id, disabled_by=None - ) - await hass.async_block_till_done() - assert update_entry != entry - assert update_entry.disabled is False - state = hass.states.get("sensor.home_wi_fi_rssi") - assert state is None - - update_entry = entity_registry.async_update_entity( - entry.entity_id, disabled_by=None - ) - await hass.async_block_till_done() - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - await hass.async_block_till_done() - - state = hass.states.get("sensor.home_wi_fi_rssi") - assert state - - entry = entity_registry.async_get("sensor.home_ble_strength") - await hass.async_block_till_done() - assert entry - assert entry.disabled is False - assert entry.disabled_by is None - state = hass.states.get("sensor.home_ble_strength") - assert state