Skip to content

Commit

Permalink
New official genie garage integration (home-assistant#117020)
Browse files Browse the repository at this point in the history
* new official genie garage integration

* move api constants into api module

* move scan interval constant to cover.py
  • Loading branch information
swcloudgenie authored May 29, 2024
1 parent f93a312 commit a670169
Show file tree
Hide file tree
Showing 25 changed files with 285 additions and 1,353 deletions.
6 changes: 6 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 35 additions & 30 deletions homeassistant/components/aladdin_connect/__init__.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,53 @@
"""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 .const import CLIENT_ID, DOMAIN
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow

_LOGGER: Final = logging.getLogger(__name__)
from . import api
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION

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


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
31 changes: 31 additions & 0 deletions homeassistant/components/aladdin_connect/api.py
Original file line number Diff line number Diff line change
@@ -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"])
Original file line number Diff line number Diff line change
@@ -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,
)
149 changes: 35 additions & 114 deletions homeassistant/components/aladdin_connect/config_flow.py
Original file line number Diff line number Diff line change
@@ -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 .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})


async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
"""Validate the user input allows us to connect.
from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.helpers import config_entry_oauth2_flow

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
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN

except Aladdin.InvalidPasswordError as ex:
raise InvalidAuth from ex

class OAuth2FlowHandler(
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
):
"""Config flow to handle Aladdin Connect Genie OAuth2 authentication."""

class AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Aladdin Connect."""
DOMAIN = DOMAIN
VERSION = CONFIG_FLOW_VERSION
MINOR_VERSION = CONFIG_FLOW_MINOR_VERSION

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
) -> 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
self, user_input: Mapping[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({}),
)

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
return await self.async_step_user()

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 self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

return await super().async_oauth_create_entry(data)

class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
@property
def logger(self) -> logging.Logger:
"""Return logger."""
return logging.getLogger(__name__)
20 changes: 6 additions & 14 deletions homeassistant/components/aladdin_connect/const.py
Original file line number Diff line number Diff line change
@@ -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"
DOMAIN = "aladdin_connect"
CONFIG_FLOW_VERSION = 2
CONFIG_FLOW_MINOR_VERSION = 1

STATES_MAP: Final[dict[str, str]] = {
"open": STATE_OPEN,
"opening": STATE_OPENING,
"closed": STATE_CLOSED,
"closing": STATE_CLOSING,
}
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.com/login.html"
OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token"

DOMAIN = "aladdin_connect"
SUPPORTED_FEATURES: Final = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
CLIENT_ID = "1000"
Loading

0 comments on commit a670169

Please sign in to comment.