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 all 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
167 changes: 101 additions & 66 deletions homeassistant/components/point/__init__.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,46 @@
"""Support for Minut Point."""

import asyncio
from dataclasses import dataclass
from http import HTTPStatus
import logging

from aiohttp import web
from httpx import ConnectTimeout
from aiohttp import ClientError, ClientResponseError, web
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 @@ -45,11 +53,10 @@

_LOGGER = logging.getLogger(__name__)

DATA_CONFIG_ENTRY_LOCK = "point_config_entry_lock"
CONFIG_ENTRY_IS_SETUP = "point_config_entry_is_setup"

PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]

type PointConfigEntry = ConfigEntry[PointData]

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
Expand All @@ -70,57 +77,80 @@

conf = config[DOMAIN]

config_flow.register_flow_implementation(
hass, DOMAIN, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET]
async_create_issue(

Check warning on line 80 in homeassistant/components/point/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/__init__.py#L80

Added line #L80 was not covered by tests
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2025.4.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Point",
},
)

hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
if not hass.config_entries.async_entries(DOMAIN):
await async_import_client_credential(

Check warning on line 96 in homeassistant/components/point/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/__init__.py#L95-L96

Added lines #L95 - L96 were not covered by tests
hass,
DOMAIN,
ClientCredential(
conf[CONF_CLIENT_ID],
conf[CONF_CLIENT_SECRET],
),
)

hass.async_create_task(

Check warning on line 105 in homeassistant/components/point/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/__init__.py#L105

Added line #L105 was not covered by tests
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
)
)
)

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."""

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}
)
if "auth_implementation" not in entry.data:
raise ConfigEntryAuthFailed("Authentication failed. Please re-authenticate.")

Check warning on line 118 in homeassistant/components/point/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/__init__.py#L117-L118

Added lines #L117 - L118 were not covered by tests

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 = (

Check warning on line 120 in homeassistant/components/point/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/__init__.py#L120

Added line #L120 was not covered by tests
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
auth = api.AsyncConfigEntryAuth(

Check warning on line 126 in homeassistant/components/point/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/__init__.py#L125-L126

Added lines #L125 - L126 were not covered by tests
aiohttp_client.async_get_clientsession(hass), session
)

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:

Check warning on line 136 in homeassistant/components/point/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/__init__.py#L131-L136

Added lines #L131 - L136 were not covered by tests
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()
point_session = PointSession(auth)

Check warning on line 139 in homeassistant/components/point/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/__init__.py#L139

Added line #L139 was not covered by tests

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, point_session)

Check warning on line 141 in homeassistant/components/point/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/__init__.py#L141

Added line #L141 was not covered by tests
hass.async_create_task(client.update())
entry.runtime_data = PointData(client)

Check warning on line 143 in homeassistant/components/point/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/__init__.py#L143

Added line #L143 was not covered by tests

await async_setup_webhook(hass, entry, point_session)

Check warning on line 145 in homeassistant/components/point/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/__init__.py#L145

Added line #L145 was not covered by tests
# Entries are added in the client.update() function.

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()
Expand All @@ -135,27 +165,26 @@
CONF_WEBHOOK_URL: webhook_url,
},
)

await session.update_webhook(
entry.data[CONF_WEBHOOK_URL],
webhook.async_generate_url(hass, entry.data[CONF_WEBHOOK_ID]),
entry.data[CONF_WEBHOOK_ID],
["*"],
)

webhook.async_register(
hass, DOMAIN, "Point", entry.data[CONF_WEBHOOK_ID], handle_webhook
)


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, Platform.ALARM_CONTROL_PANEL]
):
session: PointSession = entry.runtime_data.client
if CONF_WEBHOOK_ID in entry.data:
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
await session.remove_webhook()

Check warning on line 187 in homeassistant/components/point/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/__init__.py#L185-L187

Added lines #L185 - L187 were not covered by tests
return unload_ok


Expand Down Expand Up @@ -205,25 +234,23 @@

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]
)
self._hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key)

async_dispatcher_send(
self._hass, POINT_DISCOVERY_NEW.format(platform, DOMAIN), device_id
)

self._is_available = True
for home_id in self._client.homes:
if home_id not in self._known_homes:
await self._hass.config_entries.async_forward_entry_setups(

Check warning on line 244 in homeassistant/components/point/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/__init__.py#L244

Added line #L244 was not covered by tests
Copy link
Member

@MartinHjelmare MartinHjelmare Sep 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're only allowed to forward the config entry to a platform once until the config entry is unloaded. This looks like it could happen more than once for this config entry.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if more devices are added, what is the best approach for adding more devices on the fly?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You already have a way to add new entities from new devices using the dispatch helper.

self._config_entry, [Platform.ALARM_CONTROL_PANEL]
)
await new_device(home_id, "alarm_control_panel")
self._known_homes.add(home_id)
for device in self._client.devices:
if device.device_id not in self._known_devices:
await self._hass.config_entries.async_forward_entry_setups(

Check warning on line 251 in homeassistant/components/point/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/__init__.py#L251

Added line #L251 was not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above.

self._config_entry, PLATFORMS
)
for platform in PLATFORMS:
await new_device(device.device_id, platform)
self._known_devices.add(device.device_id)
Expand Down Expand Up @@ -262,7 +289,7 @@

_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 @@ -284,7 +311,7 @@
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 Expand Up @@ -337,3 +364,11 @@
def last_update(self):
"""Return the last_update time for the device."""
return parse_datetime(self.device.last_update)


@dataclass
class PointData:
"""Point Data."""

client: MinutPointClient
entry_lock: asyncio.Lock = asyncio.Lock()
2 changes: 1 addition & 1 deletion homeassistant/components/point/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@

async def async_discover_home(home_id):
"""Discover and add a discovered home."""
client = hass.data[POINT_DOMAIN][config_entry.entry_id]
client = config_entry.runtime_data.client

Check warning on line 46 in homeassistant/components/point/alarm_control_panel.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/alarm_control_panel.py#L46

Added line #L46 was not covered by tests
async_add_entities([MinutPointAlarmControl(client, home_id)], True)

async_dispatcher_connect(
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

Check warning on line 19 in homeassistant/components/point/api.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/api.py#L18-L19

Added lines #L18 - L19 were not covered by tests

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()

Check warning on line 24 in homeassistant/components/point/api.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/api.py#L23-L24

Added lines #L23 - L24 were not covered by tests

return self._oauth_session.token["access_token"]

Check warning on line 26 in homeassistant/components/point/api.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/api.py#L26

Added line #L26 was not covered by tests
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,
)
2 changes: 1 addition & 1 deletion homeassistant/components/point/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@

async def async_discover_sensor(device_id):
"""Discover and add a discovered sensor."""
client = hass.data[POINT_DOMAIN][config_entry.entry_id]
client = config_entry.runtime_data.client

Check warning on line 52 in homeassistant/components/point/binary_sensor.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/point/binary_sensor.py#L52

Added line #L52 was not covered by tests
async_add_entities(
(
MinutPointBinarySensor(client, device_id, device_name)
Expand Down
Loading