mirror of
https://github.com/home-assistant/core.git
synced 2026-06-25 08:05:21 +02:00
Compare commits
6 Commits
2026.7.0b0
...
rc
| Author | SHA1 | Date | |
|---|---|---|---|
| 4022eb93de | |||
| 28171dfe90 | |||
| e9af932fbe | |||
| 3212e0f051 | |||
| 87f0720450 | |||
| 534ff3f3dc |
@@ -17,7 +17,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
)
|
||||
|
||||
from .auth import DropboxConfigEntryAuth
|
||||
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN, OAUTH2_SCOPES
|
||||
|
||||
type DropboxConfigEntry = ConfigEntry[DropboxAPIClient]
|
||||
|
||||
@@ -31,6 +31,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: DropboxConfigEntry) -> b
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
|
||||
token = entry.data["token"]
|
||||
if not set(token.get("scope", "").split()).issuperset(OAUTH2_SCOPES):
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_scopes",
|
||||
)
|
||||
if "refresh_token" not in token:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_refresh_token",
|
||||
)
|
||||
|
||||
oauth2_session = OAuth2Session(hass, entry, oauth2_implementation)
|
||||
|
||||
auth = DropboxConfigEntryAuth(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Application credentials platform for the Dropbox integration."""
|
||||
|
||||
from typing import override
|
||||
|
||||
from homeassistant.components.application_credentials import ClientCredential
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
@@ -9,14 +7,14 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
LocalOAuth2ImplementationWithPkce,
|
||||
)
|
||||
|
||||
from .const import OAUTH2_AUTHORIZE, OAUTH2_SCOPES, OAUTH2_TOKEN
|
||||
from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
||||
|
||||
|
||||
async def async_get_auth_implementation(
|
||||
hass: HomeAssistant, auth_domain: str, credential: ClientCredential
|
||||
) -> AbstractOAuth2Implementation:
|
||||
"""Return custom auth implementation."""
|
||||
return DropboxOAuth2Implementation(
|
||||
"""Return auth implementation."""
|
||||
return LocalOAuth2ImplementationWithPkce(
|
||||
hass,
|
||||
auth_domain,
|
||||
credential.client_id,
|
||||
@@ -24,21 +22,3 @@ async def async_get_auth_implementation(
|
||||
OAUTH2_TOKEN,
|
||||
credential.client_secret,
|
||||
)
|
||||
|
||||
|
||||
class DropboxOAuth2Implementation(LocalOAuth2ImplementationWithPkce):
|
||||
"""Custom Dropbox OAuth2 implementation.
|
||||
|
||||
Adds the necessary authorize url parameters.
|
||||
"""
|
||||
|
||||
@property
|
||||
@override
|
||||
def extra_authorize_data(self) -> dict:
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
data: dict = {
|
||||
"token_access_type": "offline",
|
||||
"scope": " ".join(OAUTH2_SCOPES),
|
||||
}
|
||||
data.update(super().extra_authorize_data)
|
||||
return data
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
|
||||
|
||||
from .auth import DropboxConfigFlowAuth
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, OAUTH2_SCOPES
|
||||
|
||||
|
||||
class DropboxConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
@@ -26,6 +26,15 @@ class DropboxConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
"""Return logger."""
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
@property
|
||||
@override
|
||||
def extra_authorize_data(self) -> dict:
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
return {
|
||||
"token_access_type": "offline",
|
||||
"scope": " ".join(OAUTH2_SCOPES),
|
||||
}
|
||||
|
||||
@override
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Create an entry for the flow, or update existing entry."""
|
||||
@@ -51,6 +60,9 @@ class DropboxConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth upon an API authentication error."""
|
||||
token = entry_data[CONF_TOKEN]
|
||||
if not set(token.get("scope", "").split()).issuperset(OAUTH2_SCOPES):
|
||||
return await self.async_step_reauth_permissions()
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
@@ -60,3 +72,11 @@ class DropboxConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_reauth_permissions(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that additional permissions are required."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_permissions")
|
||||
return await self.async_step_user()
|
||||
|
||||
@@ -12,6 +12,7 @@ OAUTH2_SCOPES = [
|
||||
"account_info.read",
|
||||
"files.content.read",
|
||||
"files.content.write",
|
||||
"files.metadata.read",
|
||||
]
|
||||
|
||||
DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey(
|
||||
|
||||
@@ -24,10 +24,20 @@
|
||||
"reauth_confirm": {
|
||||
"description": "The Dropbox integration needs to re-authenticate your account.",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
},
|
||||
"reauth_permissions": {
|
||||
"description": "The Dropbox integration requires additional permissions to function correctly.",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"missing_refresh_token": {
|
||||
"message": "[%key:component::dropbox::config::step::reauth_confirm::description%]"
|
||||
},
|
||||
"missing_scopes": {
|
||||
"message": "[%key:component::dropbox::config::step::reauth_permissions::description%]"
|
||||
},
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.13.1"]
|
||||
"requirements": ["homematicip==2.13.2"]
|
||||
}
|
||||
|
||||
@@ -67,12 +67,13 @@ class Tami4ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
otp = user_input["otp"]
|
||||
try:
|
||||
refresh_token = await self.hass.async_add_executor_job(
|
||||
Tami4EdgeAPI.submit_otp, self.phone, otp
|
||||
)
|
||||
# pylint: disable-next=home-assistant-sequential-executor-jobs
|
||||
api = await self.hass.async_add_executor_job(
|
||||
Tami4EdgeAPI, refresh_token
|
||||
|
||||
def _submit_otp_and_create_api() -> tuple[str, Tami4EdgeAPI]:
|
||||
refresh_token = Tami4EdgeAPI.submit_otp(self.phone, otp)
|
||||
return refresh_token, Tami4EdgeAPI(refresh_token)
|
||||
|
||||
refresh_token, api = await self.hass.async_add_executor_job(
|
||||
_submit_otp_and_create_api
|
||||
)
|
||||
except exceptions.OTPFailedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
@@ -62,10 +62,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: VeraConfigEntry) -> bool
|
||||
controller = veraApi.VeraController(base_url, subscription_registry)
|
||||
|
||||
try:
|
||||
all_devices = await hass.async_add_executor_job(controller.get_devices)
|
||||
|
||||
# pylint: disable-next=home-assistant-sequential-executor-jobs
|
||||
all_scenes = await hass.async_add_executor_job(controller.get_scenes)
|
||||
def _get_devices_and_scenes():
|
||||
"""Get devices and scenes from the Vera controller."""
|
||||
return controller.get_devices(), controller.get_scenes()
|
||||
|
||||
all_devices, all_scenes = await hass.async_add_executor_job(
|
||||
_get_devices_and_scenes
|
||||
)
|
||||
except RequestException as exception:
|
||||
# There was a network related error connecting to the Vera controller.
|
||||
_LOGGER.exception("Error communicating with Vera API")
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"universal_silabs_flasher",
|
||||
"serialx"
|
||||
],
|
||||
"requirements": ["zha==2.0.0", "zha-quirks==2.0.0"],
|
||||
"requirements": ["zha==2.0.0", "zha-quirks==2.1.0"],
|
||||
"usb": [
|
||||
{
|
||||
"description": "*2652*",
|
||||
|
||||
@@ -407,6 +407,9 @@
|
||||
"reset_alarm": {
|
||||
"name": "Reset alarm"
|
||||
},
|
||||
"reset_energy": {
|
||||
"name": "Reset energy"
|
||||
},
|
||||
"reset_frost_lock": {
|
||||
"name": "Frost lock reset"
|
||||
},
|
||||
@@ -1387,6 +1390,12 @@
|
||||
"status_indication": {
|
||||
"name": "Status indication"
|
||||
},
|
||||
"switch_action_l1": {
|
||||
"name": "Switch action L1"
|
||||
},
|
||||
"switch_action_l2": {
|
||||
"name": "Switch action L2"
|
||||
},
|
||||
"switch_actions": {
|
||||
"name": "Switch actions"
|
||||
},
|
||||
@@ -1399,6 +1408,12 @@
|
||||
"switch_type": {
|
||||
"name": "Switch type"
|
||||
},
|
||||
"switch_type_l1": {
|
||||
"name": "Switch type L1"
|
||||
},
|
||||
"switch_type_l2": {
|
||||
"name": "Switch type L2"
|
||||
},
|
||||
"temperature_display_mode": {
|
||||
"name": "Temperature display mode"
|
||||
},
|
||||
|
||||
@@ -22,7 +22,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2026
|
||||
MINOR_VERSION: Final = 7
|
||||
PATCH_VERSION: Final = "0b0"
|
||||
PATCH_VERSION: Final = "0b1"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.7.0b0"
|
||||
version = "2026.7.0b1"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
|
||||
Generated
+2
-2
@@ -1284,7 +1284,7 @@ homekit-audio-proxy==1.2.1
|
||||
homelink-integration-api==0.0.5
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==2.13.1
|
||||
homematicip==2.13.2
|
||||
|
||||
# homeassistant.components.homevolt
|
||||
homevolt==0.5.0
|
||||
@@ -3453,7 +3453,7 @@ zeroconf==0.150.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==2.0.0
|
||||
zha-quirks==2.1.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==2.0.0
|
||||
|
||||
@@ -128,6 +128,54 @@ async def test_already_configured(
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
@pytest.mark.parametrize(
|
||||
("token", "expected_step"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"access_token": "mock-access-token",
|
||||
"expires_at": 9_999_999_999,
|
||||
"scope": " ".join(OAUTH2_SCOPES),
|
||||
},
|
||||
"reauth_confirm",
|
||||
),
|
||||
(
|
||||
{
|
||||
"access_token": "mock-access-token",
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"expires_at": 9_999_999_999,
|
||||
"scope": "account_info.read files.content.read files.content.write",
|
||||
},
|
||||
"reauth_permissions",
|
||||
),
|
||||
],
|
||||
ids=["missing_refresh_token", "missing_scope"],
|
||||
)
|
||||
async def test_reauth_confirm_step(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry,
|
||||
token: dict[str, object],
|
||||
expected_step: str,
|
||||
) -> None:
|
||||
"""Test reauth shows the correct confirmation step for the broken token."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
hass.config_entries.async_update_entry(
|
||||
mock_config_entry, data={**mock_config_entry.data, "token": token}
|
||||
)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == expected_step
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result["type"] is FlowResultType.EXTERNAL_STEP
|
||||
assert result["step_id"] == "auth"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"""Test the Dropbox integration setup."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from python_dropbox_api import DropboxAuthException, DropboxUnknownException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.components.dropbox.const import DOMAIN, OAUTH2_SCOPES
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
@@ -80,6 +82,46 @@ async def test_setup_entry_implementation_unavailable(
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_dropbox_client")
|
||||
@pytest.mark.parametrize(
|
||||
"token",
|
||||
[
|
||||
{
|
||||
"access_token": "mock-access-token",
|
||||
"expires_at": 9_999_999_999,
|
||||
"scope": " ".join(OAUTH2_SCOPES),
|
||||
},
|
||||
{
|
||||
"access_token": "mock-access-token",
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"expires_at": 9_999_999_999,
|
||||
"scope": "account_info.read files.content.read files.content.write",
|
||||
},
|
||||
],
|
||||
ids=["missing_refresh_token", "missing_scope"],
|
||||
)
|
||||
async def test_setup_entry_triggers_reauth(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
token: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test that a broken token triggers a reauth flow during setup."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
hass.config_entries.async_update_entry(
|
||||
mock_config_entry, data={**mock_config_entry.data, "token": token}
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
||||
assert len(flows) == 1
|
||||
assert flows[0]["context"]["source"] == SOURCE_REAUTH
|
||||
assert flows[0]["context"]["entry_id"] == mock_config_entry.entry_id
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_dropbox_client")
|
||||
async def test_unload_entry(
|
||||
hass: HomeAssistant,
|
||||
|
||||
Reference in New Issue
Block a user