Skip to content

Commit

Permalink
Guide users to migrate from Ubiquiti Cloud Accounts to local for UniF…
Browse files Browse the repository at this point in the history
…i Protect (home-assistant#111018)

Co-authored-by: J. Nick Koston <nick@koston.org>
  • Loading branch information
AngellusMortis and bdraco authored Feb 21, 2024
1 parent fb04df5 commit 7eb6614
Show file tree
Hide file tree
Showing 9 changed files with 260 additions and 40 deletions.
17 changes: 16 additions & 1 deletion homeassistant/components/unifiprotect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry)

try:
nvr_info = await protect.get_nvr()
bootstrap = await protect.get_bootstrap()
nvr_info = bootstrap.nvr
except NotAuthorized as err:
retry_key = f"{entry.entry_id}_auth"
retries = hass.data.setdefault(DOMAIN, {}).get(retry_key, 0)
Expand All @@ -73,6 +74,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except (TimeoutError, ClientError, ServerDisconnectedError) as err:
raise ConfigEntryNotReady from err

auth_user = bootstrap.users.get(bootstrap.auth_user_id)
if auth_user and auth_user.cloud_account:
ir.async_create_issue(
hass,
DOMAIN,
"cloud_user",
is_fixable=True,
is_persistent=False,
learn_more_url="https://www.home-assistant.io/integrations/unifiprotect/#local-user",
severity=IssueSeverity.ERROR,
translation_key="cloud_user",
data={"entry_id": entry.entry_id},
)

if nvr_info.version < MIN_REQUIRED_PROTECT_V:
_LOGGER.error(
OUTDATED_LOG_MESSAGE,
Expand Down
7 changes: 6 additions & 1 deletion homeassistant/components/unifiprotect/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,8 @@ async def _async_get_nvr_data(
errors = {}
nvr_data = None
try:
nvr_data = await protect.get_nvr()
bootstrap = await protect.get_bootstrap()
nvr_data = bootstrap.nvr
except NotAuthorized as ex:
_LOGGER.debug(ex)
errors[CONF_PASSWORD] = "invalid_auth"
Expand All @@ -272,6 +273,10 @@ async def _async_get_nvr_data(
)
errors["base"] = "protect_version"

auth_user = bootstrap.users.get(bootstrap.auth_user_id)
if auth_user and auth_user.cloud_account:
errors["base"] = "cloud_user"

return nvr_data, errors

async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
Expand Down
46 changes: 42 additions & 4 deletions homeassistant/components/unifiprotect/repairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
_LOGGER = logging.getLogger(__name__)


class EAConfirm(RepairsFlow):
class ProtectRepair(RepairsFlow):
"""Handler for an issue fixing flow."""

_api: ProtectApiClient
Expand All @@ -34,14 +34,20 @@ def __init__(self, api: ProtectApiClient, entry: ConfigEntry) -> None:
super().__init__()

@callback
def _async_get_placeholders(self) -> dict[str, str] | None:
def _async_get_placeholders(self) -> dict[str, str]:
issue_registry = async_get_issue_registry(self.hass)
description_placeholders = None
description_placeholders = {}
if issue := issue_registry.async_get_issue(self.handler, self.issue_id):
description_placeholders = issue.translation_placeholders
description_placeholders = issue.translation_placeholders or {}
if issue.learn_more_url:
description_placeholders["learn_more"] = issue.learn_more_url

return description_placeholders


class EAConfirm(ProtectRepair):
"""Handler for an issue fixing flow."""

async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
Expand Down Expand Up @@ -85,6 +91,33 @@ async def async_step_confirm(
)


class CloudAccount(ProtectRepair):
"""Handler for an issue fixing flow."""

async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the first step of a fix flow."""

return await self.async_step_confirm()

async def async_step_confirm(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the first step of a fix flow."""

if user_input is None:
placeholders = self._async_get_placeholders()
return self.async_show_form(
step_id="confirm",
data_schema=vol.Schema({}),
description_placeholders=placeholders,
)

self._entry.async_start_reauth(self.hass)
return self.async_create_entry(data={})


async def async_create_fix_flow(
hass: HomeAssistant,
issue_id: str,
Expand All @@ -96,4 +129,9 @@ async def async_create_fix_flow(
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
api = async_create_api_client(hass, entry)
return EAConfirm(api, entry)
elif data is not None and issue_id == "cloud_user":
entry_id = cast(str, data["entry_id"])
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
api = async_create_api_client(hass, entry)
return CloudAccount(api, entry)
return ConfirmRepairFlow()
14 changes: 13 additions & 1 deletion homeassistant/components/unifiprotect/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"protect_version": "Minimum required version is v1.20.0. Please upgrade UniFi Protect and then retry."
"protect_version": "Minimum required version is v1.20.0. Please upgrade UniFi Protect and then retry.",
"cloud_user": "Ubiquiti Cloud users are not Supported. Please use a Local only user."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
Expand Down Expand Up @@ -78,6 +79,17 @@
"ea_setup_failed": {
"title": "Setup error using Early Access version",
"description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please [downgrade to a stable version](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) of UniFi Protect to continue using the integration.\n\nError: {error}"
},
"cloud_user": {
"title": "Ubiquiti Cloud Users are not Supported",
"fix_flow": {
"step": {
"confirm": {
"title": "Ubiquiti Cloud Users are not Supported",
"description": "Starting on July 22nd, 2024, Ubiquiti will require all cloud users to enroll in multi-factor authentication (MFA), which is incompatible with Home Assistant.\n\nIt would be best to migrate to using a [local user]({learn_more}) as soon as possible to keep the integration working.\n\nConfirming this repair will trigger a re-authentication flow to enter the needed authentication credentials."
}
}
}
}
},
"entity": {
Expand Down
18 changes: 18 additions & 0 deletions tests/components/unifiprotect/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
Bootstrap,
Camera,
Chime,
CloudAccount,
Doorlock,
Light,
Liveview,
Expand Down Expand Up @@ -119,6 +120,7 @@ def mock_ufp_client(bootstrap: Bootstrap):
client.base_url = "https://127.0.0.1"
client.connection_host = IPv4Address("127.0.0.1")
client.get_nvr = AsyncMock(return_value=nvr)
client.get_bootstrap = AsyncMock(return_value=bootstrap)
client.update = AsyncMock(return_value=bootstrap)
client.async_disconnect_ws = AsyncMock()
return client
Expand Down Expand Up @@ -345,3 +347,19 @@ def chime():
def fixed_now_fixture():
"""Return datetime object that will be consistent throughout test."""
return dt_util.utcnow()


@pytest.fixture(name="cloud_account")
def cloud_account() -> CloudAccount:
"""Return UI Cloud Account."""

return CloudAccount(
id="42",
first_name="Test",
last_name="User",
email="test@example.com",
user_id="42",
name="Test User",
location=None,
profile_img=None,
)
Loading

0 comments on commit 7eb6614

Please sign in to comment.