-
-
Notifications
You must be signed in to change notification settings - Fork 31.7k
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
Changes from 39 commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
6c59c80
initial oauth2 implementation
fredrike cfdf377
fix unload_entry
fredrike 059b1d1
read old yaml/entry config
fredrike a57b284
Merge branch 'dev' into point-oauth
fredrike 2fb90fe
update tests
fredrike b97699d
Merge remote-tracking branch 'upstream/dev' into point-oauth
fredrike 28efe88
Merge remote-tracking branch 'upstream/dev' into point-oauth
fredrike c99f0ff
Merge branch 'dev' into point-oauth
fredrike f0bf932
fix: pylint on tests
fredrike 903485e
Merge branch 'dev' into point-oauth
fredrike 669664b
Merge branch 'dev' into point-oauth
fredrike 31d313e
Apply suggestions from code review
fredrike 04f1ca3
fix constants, formatting
fredrike 7de2439
use runtime_data
fredrike 097de14
Merge branch 'dev' into point-oauth
fredrike 61651b5
Merge branch 'dev' into point-oauth
fredrike 039e425
Merge remote-tracking branch 'upstream/dev' into point-oauth
fredrike 38d5751
Apply suggestions from code review
fredrike 7f52656
fix missing import
fredrike d50f5e0
adopt to PointData dataclass
fredrike 2fc1c63
fix typing
fredrike 756f496
Merge branch 'dev' into point-oauth
fredrike fb36135
add more strings (copied from weheat)
fredrike 8613918
move the PointData dataclass to avoid circular imports
fredrike 596831b
use configflow inspired by withings
fredrike 8a23fec
Merge branch 'dev' of https://github.com/home-assistant/core into poi…
fredrike 682f1dc
Merge branch 'dev' of https://github.com/home-assistant/core into poi…
fredrike c518b40
raise ConfigEntryAuthFailed
fredrike d510cb9
it is called entry_lock
fredrike e29dc23
fix webhook issue
fredrike 72ed8ac
fix oauth_create_entry
fredrike f407cc9
stop using async_forward_entry_setup
fredrike 716c7ac
Merge branch 'dev' into point-oauth
joostlek 26a0682
Fixup
joostlek 7480149
Merge branch 'dev' into point-oauth
joostlek bad8fb7
Merge branches 'point-oauth' and 'point-oauth' of github.com:fredrike…
fredrike f07d34d
fix strings
fredrike b3979a4
fix issue that old config might be without unique_id
fredrike 58a21ea
parametrize tests
fredrike 6b7bd29
Update homeassistant/components/point/config_flow.py
joostlek b9bcb93
Update tests/components/point/test_config_flow.py
joostlek 0a8e7ef
Fix
joostlek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
|
@@ -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( | ||
|
@@ -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( | ||
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( | ||
hass, | ||
DOMAIN, | ||
ClientCredential( | ||
conf[CONF_CLIENT_ID], | ||
conf[CONF_CLIENT_SECRET], | ||
), | ||
) | ||
|
||
hass.async_create_task( | ||
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.") | ||
|
||
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 | ||
) | ||
|
||
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() | ||
point_session = PointSession(auth) | ||
|
||
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) | ||
hass.async_create_task(client.update()) | ||
entry.runtime_data = PointData(client) | ||
|
||
await async_setup_webhook(hass, entry, point_session) | ||
# 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() | ||
|
@@ -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() | ||
return unload_ok | ||
|
||
|
||
|
@@ -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( | ||
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
@@ -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 | ||
|
@@ -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}" | ||
|
||
|
@@ -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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.