Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for native oauth2 in Point #118243

Merged
merged 42 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6c59c80
initial oauth2 implementation
fredrike May 27, 2024
cfdf377
fix unload_entry
fredrike May 28, 2024
059b1d1
read old yaml/entry config
fredrike May 28, 2024
a57b284
Merge branch 'dev' into point-oauth
fredrike May 28, 2024
2fb90fe
update tests
fredrike May 29, 2024
b97699d
Merge remote-tracking branch 'upstream/dev' into point-oauth
fredrike Jul 2, 2024
28efe88
Merge remote-tracking branch 'upstream/dev' into point-oauth
fredrike Jul 19, 2024
c99f0ff
Merge branch 'dev' into point-oauth
fredrike Jul 19, 2024
f0bf932
fix: pylint on tests
fredrike Jul 19, 2024
903485e
Merge branch 'dev' into point-oauth
fredrike Jul 19, 2024
669664b
Merge branch 'dev' into point-oauth
fredrike Aug 2, 2024
31d313e
Apply suggestions from code review
fredrike Aug 9, 2024
04f1ca3
fix constants, formatting
fredrike Aug 14, 2024
7de2439
use runtime_data
fredrike Aug 14, 2024
097de14
Merge branch 'dev' into point-oauth
fredrike Aug 14, 2024
61651b5
Merge branch 'dev' into point-oauth
fredrike Aug 14, 2024
039e425
Merge remote-tracking branch 'upstream/dev' into point-oauth
fredrike Aug 22, 2024
38d5751
Apply suggestions from code review
fredrike Sep 11, 2024
7f52656
fix missing import
fredrike Sep 11, 2024
d50f5e0
adopt to PointData dataclass
fredrike Sep 11, 2024
2fc1c63
fix typing
fredrike Sep 12, 2024
756f496
Merge branch 'dev' into point-oauth
fredrike Sep 14, 2024
fb36135
add more strings (copied from weheat)
fredrike Sep 14, 2024
8613918
move the PointData dataclass to avoid circular imports
fredrike Sep 14, 2024
596831b
use configflow inspired by withings
fredrike Sep 14, 2024
8a23fec
Merge branch 'dev' of https://github.com/home-assistant/core into poi…
fredrike Sep 16, 2024
682f1dc
Merge branch 'dev' of https://github.com/home-assistant/core into poi…
fredrike Sep 19, 2024
c518b40
raise ConfigEntryAuthFailed
fredrike Sep 19, 2024
d510cb9
it is called entry_lock
fredrike Sep 19, 2024
e29dc23
fix webhook issue
fredrike Sep 19, 2024
72ed8ac
fix oauth_create_entry
fredrike Sep 19, 2024
f407cc9
stop using async_forward_entry_setup
fredrike Sep 19, 2024
716c7ac
Merge branch 'dev' into point-oauth
joostlek Sep 19, 2024
26a0682
Fixup
joostlek Sep 19, 2024
7480149
Merge branch 'dev' into point-oauth
joostlek Sep 19, 2024
bad8fb7
Merge branches 'point-oauth' and 'point-oauth' of github.com:fredrike…
fredrike Sep 19, 2024
f07d34d
fix strings
fredrike Sep 19, 2024
b3979a4
fix issue that old config might be without unique_id
fredrike Sep 19, 2024
58a21ea
parametrize tests
fredrike Sep 19, 2024
6b7bd29
Update homeassistant/components/point/config_flow.py
joostlek Sep 20, 2024
b9bcb93
Update tests/components/point/test_config_flow.py
joostlek Sep 20, 2024
0a8e7ef
Fix
joostlek Sep 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,7 @@ omit =
homeassistant/components/pocketcasts/sensor.py
homeassistant/components/point/__init__.py
homeassistant/components/point/alarm_control_panel.py
homeassistant/components/point/api.py
homeassistant/components/point/binary_sensor.py
homeassistant/components/point/sensor.py
homeassistant/components/powerwall/__init__.py
Expand Down
188 changes: 127 additions & 61 deletions homeassistant/components/point/__init__.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,46 @@
"""Support for Minut Point."""

import asyncio
from http import HTTPStatus
import logging

from httpx import ConnectTimeout
from aiohttp import ClientError, ClientResponseError
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.components.application_credentials import (
ClientCredential,
async_import_client_credential,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_TOKEN,
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.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
config_validation as cv,
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.issue_registry import IssueSeverity, async_create_issue
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,6 +58,10 @@

PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]

type PointConfigEntry = ConfigEntry[api.AsyncConfigEntryAuth]
fredrike marked this conversation as resolved.
Show resolved Hide resolved

CONF_REFRESH_TOKEN = "refresh_token"
fredrike marked this conversation as resolved.
Show resolved Hide resolved

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
Expand All @@ -69,62 +82,102 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:

conf = config[DOMAIN]

config_flow.register_flow_implementation(
hass, DOMAIN, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET]
await async_import_client_credential(
hass,
DOMAIN,
ClientCredential(
conf[CONF_CLIENT_ID],
conf[CONF_CLIENT_SECRET],
),
)

hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
)
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2024.8.0",
fredrike marked this conversation as resolved.
Show resolved Hide resolved
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Point",
},
)

await hass.config_entries.flow.async_init(
fredrike marked this conversation as resolved.
Show resolved Hide resolved
DOMAIN,
context={"source": SOURCE_IMPORT},
data=conf,
)
fredrike marked this conversation as resolved.
Show resolved Hide resolved

return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Point from a config entry."""
async def async_setup_entry(hass: HomeAssistant, entry: PointConfigEntry) -> bool:
"""Set up Minut Point from a config entry."""
hass.data.setdefault(DOMAIN, {})
fredrike marked this conversation as resolved.
Show resolved Hide resolved

async def token_saver(token, **kwargs):
_LOGGER.debug("Saving updated token %s", token)
if "auth_implementation" not in entry.data:
# Config entry is imported from old implementation without native oauth2.
fredrike marked this conversation as resolved.
Show resolved Hide resolved
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_TOKEN: token}
entry,
data={
**entry.data,
"auth_implementation": DOMAIN,
CONF_TOKEN: {
**entry.data[CONF_TOKEN],
"expires_at": 0,
},
"imported": True,
},
)
fredrike marked this conversation as resolved.
Show resolved Hide resolved

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,
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
auth = api.AsyncConfigEntryAuth(
aiohttp_client.async_get_clientsession(hass), session
)
entry.runtime_data = auth

try:
# the call to user() implicitly calls ensure_active_token() in authlib
await session.user()
except ConnectTimeout as err:
_LOGGER.debug("Connection Timeout")
await auth.async_get_access_token()
except ClientResponseError as err:
if err.status in {HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN}:
raise ConfigEntryAuthFailed from err
raise ConfigEntryNotReady from err
except ClientError as err:
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()
pointSession = PointSession(auth)
fredrike marked this conversation as resolved.
Show resolved Hide resolved

await async_setup_webhook(hass, entry, session)
client = MinutPointClient(hass, entry, session)
hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: client})
client = MinutPointClient(hass, entry, pointSession)
hass.async_create_task(client.update())
hass.data[DOMAIN][entry.entry_id] = client
fredrike marked this conversation as resolved.
Show resolved Hide resolved

hass.data[DOMAIN][DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock()
hass.data[DOMAIN][CONFIG_ENTRY_IS_SETUP] = set()
fredrike marked this conversation as resolved.
Show resolved Hide resolved

await async_setup_webhook(hass, entry, pointSession)
# Entries are added in the client.update() function.
# await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
fredrike marked this conversation as resolved.
Show resolved Hide resolved

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)
fredrike marked this conversation as resolved.
Show resolved Hide resolved

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

if await session.update_webhook(
fredrike marked this conversation as resolved.
Show resolved Hide resolved
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):
session: PointSession = hass.data[DOMAIN].pop(entry.entry_id)
if CONF_WEBHOOK_ID in entry.data:
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
await session.remove_webhook()
return unload_ok


Expand Down Expand Up @@ -203,12 +264,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]:
await self._hass.config_entries.async_forward_entry_setups(
self._config_entry, [platform]
async with self._hass.data[DOMAIN][DATA_CONFIG_ENTRY_LOCK]:
if (
config_entries_key
fredrike marked this conversation as resolved.
Show resolved Hide resolved
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[DOMAIN][CONFIG_ENTRY_IS_SETUP].add(
config_entries_key
)
self._hass.data[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 +325,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 +347,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
Loading