Skip to content

Commit

Permalink
initial oauth2 implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
fredrike committed May 27, 2024
1 parent 500684b commit 957bafe
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 291 deletions.
156 changes: 64 additions & 92 deletions homeassistant/components/point/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,27 @@
import asyncio
import logging

from httpx import ConnectTimeout
from pypoint import PointSession
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.components import webhook
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_TOKEN,
CONF_WEBHOOK_ID,
Platform,
)
from homeassistant.const import CONF_WEBHOOK_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
device_registry as dr,
)
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.dt import as_local, parse_datetime, utc_from_timestamp

from . import config_flow
from . import api
from .const import (
CONF_WEBHOOK_URL,
DOMAIN,
Expand All @@ -49,82 +41,49 @@

PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
type PointConfigEntry = ConfigEntry[api.AsyncConfigEntryAuth]


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Minut Point component."""
if DOMAIN not in config:
return True

conf = config[DOMAIN]

config_flow.register_flow_implementation(
hass, DOMAIN, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET]
)
async def async_setup_entry(hass: HomeAssistant, entry: PointConfigEntry) -> bool:
"""Set up Minut Point from a config entry."""
hass.data.setdefault(DOMAIN, {})

hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)

return True
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
auth = api.AsyncConfigEntryAuth(
aiohttp_client.async_get_clientsession(hass), session
)
entry.runtime_data = auth

_LOGGER.warning("FER, %s", await auth.async_get_access_token())
pointSession = PointSession(auth)

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Point from a config entry."""
client = MinutPointClient(hass, entry, pointSession)
hass.async_create_task(client.update())
hass.data[DOMAIN][entry.entry_id] = client

async def token_saver(token, **kwargs):
_LOGGER.debug("Saving updated token %s", token)
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_TOKEN: token}
)
hass.data[DOMAIN][DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock()
hass.data[DOMAIN][CONFIG_ENTRY_IS_SETUP] = set()

session = PointSession(
async_get_clientsession(hass),
entry.data["refresh_args"][CONF_CLIENT_ID],
entry.data["refresh_args"][CONF_CLIENT_SECRET],
token=entry.data[CONF_TOKEN],
token_saver=token_saver,
)
try:
# the call to user() implicitly calls ensure_active_token() in authlib
await session.user()
except ConnectTimeout as err:
_LOGGER.debug("Connection Timeout")
raise ConfigEntryNotReady from err
except Exception: # noqa: BLE001
_LOGGER.error("Authentication Error")
return False

hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock()
hass.data[CONFIG_ENTRY_IS_SETUP] = set()

await async_setup_webhook(hass, entry, session)
client = MinutPointClient(hass, entry, session)
hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: client})
hass.async_create_task(client.update())
await async_setup_webhook(hass, entry, pointSession)
# await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True


async def async_setup_webhook(hass: HomeAssistant, entry: ConfigEntry, session):
async def async_setup_webhook(
hass: HomeAssistant, entry: PointConfigEntry, session: PointSession
) -> None:
"""Set up a webhook to handle binary sensor events."""
if CONF_WEBHOOK_ID not in entry.data:
webhook_id = webhook.async_generate_id()
webhook_url = webhook.async_generate_url(hass, webhook_id)
_LOGGER.info("Registering new webhook at: %s", webhook_url)
_LOGGER.warning("Registering new webhook at: %s", webhook_url)

hass.config_entries.async_update_entry(
entry,
Expand All @@ -134,26 +93,34 @@ async def async_setup_webhook(hass: HomeAssistant, entry: ConfigEntry, session):
CONF_WEBHOOK_URL: webhook_url,
},
)
await session.update_webhook(

if await session.update_webhook(
entry.data[CONF_WEBHOOK_URL],
entry.data[CONF_WEBHOOK_ID],
["*"],
)

webhook.async_register(
hass, DOMAIN, "Point", entry.data[CONF_WEBHOOK_ID], handle_webhook
)
):
webhook.async_register(
hass, DOMAIN, "Point", entry.data[CONF_WEBHOOK_ID], handle_webhook
)
else:
_LOGGER.warning(
"Error registering webhook at: %s", entry.data[CONF_WEBHOOK_URL]
)
data = {**entry.data}
data.pop(CONF_WEBHOOK_ID, None)
data.pop(CONF_WEBHOOK_URL, None)
hass.config_entries.async_update_entry(
entry,
data=data,
)


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: PointConfigEntry) -> bool:
"""Unload a config entry."""
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
session = hass.data[DOMAIN].pop(entry.entry_id)
await session.remove_webhook()

unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
session: PointSession = hass.data[DOMAIN].pop(entry)
await session.remove_webhook()

return unload_ok

Expand Down Expand Up @@ -203,12 +170,17 @@ async def _sync(self):
async def new_device(device_id, platform):
"""Load new device."""
config_entries_key = f"{platform}.{DOMAIN}"
async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]:
if config_entries_key not in self._hass.data[CONFIG_ENTRY_IS_SETUP]:
async with self._hass.data[DOMAIN][DATA_CONFIG_ENTRY_LOCK]:
if (
config_entries_key
not in self._hass.data[DOMAIN][CONFIG_ENTRY_IS_SETUP]
):
await self._hass.config_entries.async_forward_entry_setup(
self._config_entry, platform
)
self._hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key)
self._hass.data[DOMAIN][CONFIG_ENTRY_IS_SETUP].add(
config_entries_key
)

async_dispatcher_send(
self._hass, POINT_DISCOVERY_NEW.format(platform, DOMAIN), device_id
Expand Down Expand Up @@ -259,7 +231,7 @@ class MinutPointEntity(Entity):

_attr_should_poll = False

def __init__(self, point_client, device_id, device_class):
def __init__(self, point_client, device_id, device_class) -> None:
"""Initialize the entity."""
self._async_unsub_dispatcher_connect = None
self._client = point_client
Expand All @@ -281,7 +253,7 @@ def __init__(self, point_client, device_id, device_class):
if device_class:
self._attr_name = f"{self._name} {device_class.capitalize()}"

def __str__(self):
def __str__(self) -> str:
"""Return string representation of device."""
return f"MinutPoint {self.name}"

Expand Down
26 changes: 26 additions & 0 deletions homeassistant/components/point/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""API for Minut Point bound to Home Assistant OAuth."""

from aiohttp import ClientSession
import pypoint

from homeassistant.helpers import config_entry_oauth2_flow


class AsyncConfigEntryAuth(pypoint.AbstractAuth):
"""Provide Minut Point authentication tied to an OAuth2 based config entry."""

def __init__(
self,
websession: ClientSession,
oauth_session: config_entry_oauth2_flow.OAuth2Session,
) -> None:
"""Initialize Minut Point auth."""
super().__init__(websession)
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 self._oauth_session.token["access_token"]
14 changes: 14 additions & 0 deletions homeassistant/components/point/application_credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""application_credentials platform the Minut Point 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,
)
Loading

0 comments on commit 957bafe

Please sign in to comment.