Guide users to migrate from Ubiquiti Cloud Accounts to local for UniFi Protect (#111018)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Christopher Bailey 2024-02-21 00:32:47 -05:00 committed by GitHub
parent fb04df5392
commit 7eb6614818
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 260 additions and 40 deletions

View file

@ -61,7 +61,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry) data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry)
try: try:
nvr_info = await protect.get_nvr() bootstrap = await protect.get_bootstrap()
nvr_info = bootstrap.nvr
except NotAuthorized as err: except NotAuthorized as err:
retry_key = f"{entry.entry_id}_auth" retry_key = f"{entry.entry_id}_auth"
retries = hass.data.setdefault(DOMAIN, {}).get(retry_key, 0) retries = hass.data.setdefault(DOMAIN, {}).get(retry_key, 0)
@ -73,6 +74,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except (TimeoutError, ClientError, ServerDisconnectedError) as err: except (TimeoutError, ClientError, ServerDisconnectedError) as err:
raise ConfigEntryNotReady from 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: if nvr_info.version < MIN_REQUIRED_PROTECT_V:
_LOGGER.error( _LOGGER.error(
OUTDATED_LOG_MESSAGE, OUTDATED_LOG_MESSAGE,

View file

@ -256,7 +256,8 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors = {} errors = {}
nvr_data = None nvr_data = None
try: try:
nvr_data = await protect.get_nvr() bootstrap = await protect.get_bootstrap()
nvr_data = bootstrap.nvr
except NotAuthorized as ex: except NotAuthorized as ex:
_LOGGER.debug(ex) _LOGGER.debug(ex)
errors[CONF_PASSWORD] = "invalid_auth" errors[CONF_PASSWORD] = "invalid_auth"
@ -272,6 +273,10 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
) )
errors["base"] = "protect_version" 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 return nvr_data, errors
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:

View file

@ -20,7 +20,7 @@ from .utils import async_create_api_client
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class EAConfirm(RepairsFlow): class ProtectRepair(RepairsFlow):
"""Handler for an issue fixing flow.""" """Handler for an issue fixing flow."""
_api: ProtectApiClient _api: ProtectApiClient
@ -34,14 +34,20 @@ class EAConfirm(RepairsFlow):
super().__init__() super().__init__()
@callback @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) 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): 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 return description_placeholders
class EAConfirm(ProtectRepair):
"""Handler for an issue fixing flow."""
async def async_step_init( async def async_step_init(
self, user_input: dict[str, str] | None = None self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult: ) -> data_entry_flow.FlowResult:
@ -85,6 +91,33 @@ class EAConfirm(RepairsFlow):
) )
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( async def async_create_fix_flow(
hass: HomeAssistant, hass: HomeAssistant,
issue_id: str, issue_id: str,
@ -96,4 +129,9 @@ async def async_create_fix_flow(
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None: if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
api = async_create_api_client(hass, entry) api = async_create_api_client(hass, entry)
return EAConfirm(api, 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() return ConfirmRepairFlow()

View file

@ -37,7 +37,8 @@
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "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": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
@ -78,6 +79,17 @@
"ea_setup_failed": { "ea_setup_failed": {
"title": "Setup error using Early Access version", "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}" "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": { "entity": {

View file

@ -19,6 +19,7 @@ from pyunifiprotect.data import (
Bootstrap, Bootstrap,
Camera, Camera,
Chime, Chime,
CloudAccount,
Doorlock, Doorlock,
Light, Light,
Liveview, Liveview,
@ -119,6 +120,7 @@ def mock_ufp_client(bootstrap: Bootstrap):
client.base_url = "https://127.0.0.1" client.base_url = "https://127.0.0.1"
client.connection_host = IPv4Address("127.0.0.1") client.connection_host = IPv4Address("127.0.0.1")
client.get_nvr = AsyncMock(return_value=nvr) client.get_nvr = AsyncMock(return_value=nvr)
client.get_bootstrap = AsyncMock(return_value=bootstrap)
client.update = AsyncMock(return_value=bootstrap) client.update = AsyncMock(return_value=bootstrap)
client.async_disconnect_ws = AsyncMock() client.async_disconnect_ws = AsyncMock()
return client return client
@ -345,3 +347,19 @@ def chime():
def fixed_now_fixture(): def fixed_now_fixture():
"""Return datetime object that will be consistent throughout test.""" """Return datetime object that will be consistent throughout test."""
return dt_util.utcnow() 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,
)

View file

@ -7,7 +7,7 @@ from unittest.mock import patch
import pytest import pytest
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
from pyunifiprotect.data import NVR from pyunifiprotect.data import NVR, Bootstrap, CloudAccount
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import dhcp, ssdp from homeassistant.components import dhcp, ssdp
@ -57,7 +57,7 @@ UNIFI_DISCOVERY_DICT = asdict(UNIFI_DISCOVERY)
UNIFI_DISCOVERY_DICT_PARTIAL = asdict(UNIFI_DISCOVERY_PARTIAL) UNIFI_DISCOVERY_DICT_PARTIAL = asdict(UNIFI_DISCOVERY_PARTIAL)
async def test_form(hass: HomeAssistant, nvr: NVR) -> None: async def test_form(hass: HomeAssistant, bootstrap: Bootstrap, nvr: NVR) -> None:
"""Test we get the form.""" """Test we get the form."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -65,9 +65,10 @@ async def test_form(hass: HomeAssistant, nvr: NVR) -> None:
assert result["type"] == FlowResultType.FORM assert result["type"] == FlowResultType.FORM
assert not result["errors"] assert not result["errors"]
bootstrap.nvr = nvr
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
return_value=nvr, return_value=bootstrap,
), patch( ), patch(
"homeassistant.components.unifiprotect.async_setup_entry", "homeassistant.components.unifiprotect.async_setup_entry",
return_value=True, return_value=True,
@ -99,15 +100,18 @@ async def test_form(hass: HomeAssistant, nvr: NVR) -> None:
assert len(mock_setup.mock_calls) == 1 assert len(mock_setup.mock_calls) == 1
async def test_form_version_too_old(hass: HomeAssistant, old_nvr: NVR) -> None: async def test_form_version_too_old(
hass: HomeAssistant, bootstrap: Bootstrap, old_nvr: NVR
) -> None:
"""Test we handle the version being too old.""" """Test we handle the version being too old."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
bootstrap.nvr = old_nvr
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
return_value=old_nvr, return_value=bootstrap,
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@ -129,7 +133,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None:
) )
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
side_effect=NotAuthorized, side_effect=NotAuthorized,
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
@ -145,6 +149,34 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None:
assert result2["errors"] == {"password": "invalid_auth"} assert result2["errors"] == {"password": "invalid_auth"}
async def test_form_cloud_user(
hass: HomeAssistant, bootstrap: Bootstrap, cloud_account: CloudAccount
) -> None:
"""Test we handle cloud users."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
user = bootstrap.users[bootstrap.auth_user_id]
user.cloud_account = cloud_account
bootstrap.users[bootstrap.auth_user_id] = user
with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
return_value=bootstrap,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.1",
"username": "test-username",
"password": "test-password",
},
)
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == {"base": "cloud_user"}
async def test_form_cannot_connect(hass: HomeAssistant) -> None: async def test_form_cannot_connect(hass: HomeAssistant) -> None:
"""Test we handle cannot connect error.""" """Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -152,7 +184,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
) )
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
side_effect=NvrError, side_effect=NvrError,
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
@ -168,7 +200,9 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
assert result2["errors"] == {"base": "cannot_connect"} assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None: async def test_form_reauth_auth(
hass: HomeAssistant, bootstrap: Bootstrap, nvr: NVR
) -> None:
"""Test we handle reauth auth.""" """Test we handle reauth auth."""
mock_config = MockConfigEntry( mock_config = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
@ -200,7 +234,7 @@ async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None:
} }
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
side_effect=NotAuthorized, side_effect=NotAuthorized,
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
@ -215,9 +249,10 @@ async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None:
assert result2["errors"] == {"password": "invalid_auth"} assert result2["errors"] == {"password": "invalid_auth"}
assert result2["step_id"] == "reauth_confirm" assert result2["step_id"] == "reauth_confirm"
bootstrap.nvr = nvr
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
return_value=nvr, return_value=bootstrap,
), patch( ), patch(
"homeassistant.components.unifiprotect.async_setup", "homeassistant.components.unifiprotect.async_setup",
return_value=True, return_value=True,
@ -313,7 +348,7 @@ async def test_discovered_by_ssdp_or_dhcp(
async def test_discovered_by_unifi_discovery_direct_connect( async def test_discovered_by_unifi_discovery_direct_connect(
hass: HomeAssistant, nvr: NVR hass: HomeAssistant, bootstrap: Bootstrap, nvr: NVR
) -> None: ) -> None:
"""Test a discovery from unifi-discovery.""" """Test a discovery from unifi-discovery."""
@ -335,9 +370,10 @@ async def test_discovered_by_unifi_discovery_direct_connect(
assert not result["errors"] assert not result["errors"]
bootstrap.nvr = nvr
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
return_value=nvr, return_value=bootstrap,
), patch( ), patch(
"homeassistant.components.unifiprotect.async_setup_entry", "homeassistant.components.unifiprotect.async_setup_entry",
return_value=True, return_value=True,
@ -501,7 +537,9 @@ async def test_discovered_host_not_updated_if_existing_is_a_hostname(
assert mock_config.data[CONF_HOST] == "a.hostname" assert mock_config.data[CONF_HOST] == "a.hostname"
async def test_discovered_by_unifi_discovery(hass: HomeAssistant, nvr: NVR) -> None: async def test_discovered_by_unifi_discovery(
hass: HomeAssistant, bootstrap: Bootstrap, nvr: NVR
) -> None:
"""Test a discovery from unifi-discovery.""" """Test a discovery from unifi-discovery."""
with _patch_discovery(): with _patch_discovery():
@ -522,9 +560,10 @@ async def test_discovered_by_unifi_discovery(hass: HomeAssistant, nvr: NVR) -> N
assert not result["errors"] assert not result["errors"]
bootstrap.nvr = nvr
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
side_effect=[NotAuthorized, nvr], side_effect=[NotAuthorized, bootstrap],
), patch( ), patch(
"homeassistant.components.unifiprotect.async_setup_entry", "homeassistant.components.unifiprotect.async_setup_entry",
return_value=True, return_value=True,
@ -556,7 +595,7 @@ async def test_discovered_by_unifi_discovery(hass: HomeAssistant, nvr: NVR) -> N
async def test_discovered_by_unifi_discovery_partial( async def test_discovered_by_unifi_discovery_partial(
hass: HomeAssistant, nvr: NVR hass: HomeAssistant, bootstrap: Bootstrap, nvr: NVR
) -> None: ) -> None:
"""Test a discovery from unifi-discovery partial.""" """Test a discovery from unifi-discovery partial."""
@ -578,9 +617,10 @@ async def test_discovered_by_unifi_discovery_partial(
assert not result["errors"] assert not result["errors"]
bootstrap.nvr = nvr
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
return_value=nvr, return_value=bootstrap,
), patch( ), patch(
"homeassistant.components.unifiprotect.async_setup_entry", "homeassistant.components.unifiprotect.async_setup_entry",
return_value=True, return_value=True,
@ -710,7 +750,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_resolver_fails( async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_resolver_fails(
hass: HomeAssistant, nvr: NVR hass: HomeAssistant, bootstrap: Bootstrap, nvr: NVR
) -> None: ) -> None:
"""Test we can still configure if the resolver fails.""" """Test we can still configure if the resolver fails."""
mock_config = MockConfigEntry( mock_config = MockConfigEntry(
@ -751,9 +791,10 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
assert not result["errors"] assert not result["errors"]
bootstrap.nvr = nvr
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
return_value=nvr, return_value=bootstrap,
), patch( ), patch(
"homeassistant.components.unifiprotect.async_setup_entry", "homeassistant.components.unifiprotect.async_setup_entry",
return_value=True, return_value=True,

View file

@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch
import aiohttp import aiohttp
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
from pyunifiprotect.data import NVR, Bootstrap, Light from pyunifiprotect.data import NVR, Bootstrap, CloudAccount, Light
from homeassistant.components.unifiprotect.const import ( from homeassistant.components.unifiprotect.const import (
AUTH_RETRIES, AUTH_RETRIES,
@ -132,7 +132,9 @@ async def test_setup_too_old(
) -> None: ) -> None:
"""Test setup of unifiprotect entry with too old of version of UniFi Protect.""" """Test setup of unifiprotect entry with too old of version of UniFi Protect."""
ufp.api.get_nvr.return_value = old_nvr old_bootstrap = ufp.api.bootstrap.copy()
old_bootstrap.nvr = old_nvr
ufp.api.get_bootstrap.return_value = old_bootstrap
await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -140,6 +142,37 @@ async def test_setup_too_old(
assert not ufp.api.update.called assert not ufp.api.update.called
async def test_setup_cloud_account(
hass: HomeAssistant,
ufp: MockUFPFixture,
cloud_account: CloudAccount,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test setup of unifiprotect entry with cloud account."""
bootstrap = ufp.api.bootstrap
user = bootstrap.users[bootstrap.auth_user_id]
user.cloud_account = cloud_account
bootstrap.users[bootstrap.auth_user_id] = user
ufp.api.get_bootstrap.return_value = bootstrap
ws_client = await hass_ws_client(hass)
await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done()
assert ufp.entry.state == ConfigEntryState.LOADED
await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await ws_client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) > 0
issue = None
for i in msg["result"]["issues"]:
if i["issue_id"] == "cloud_user":
issue = i
assert issue is not None
async def test_setup_failed_update(hass: HomeAssistant, ufp: MockUFPFixture) -> None: async def test_setup_failed_update(hass: HomeAssistant, ufp: MockUFPFixture) -> None:
"""Test setup of unifiprotect entry with failed update.""" """Test setup of unifiprotect entry with failed update."""
@ -178,7 +211,7 @@ async def test_setup_failed_update_reauth(
async def test_setup_failed_error(hass: HomeAssistant, ufp: MockUFPFixture) -> None: async def test_setup_failed_error(hass: HomeAssistant, ufp: MockUFPFixture) -> None:
"""Test setup of unifiprotect entry with generic error.""" """Test setup of unifiprotect entry with generic error."""
ufp.api.get_nvr = AsyncMock(side_effect=NvrError) ufp.api.get_bootstrap = AsyncMock(side_effect=NvrError)
await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -189,7 +222,7 @@ async def test_setup_failed_error(hass: HomeAssistant, ufp: MockUFPFixture) -> N
async def test_setup_failed_auth(hass: HomeAssistant, ufp: MockUFPFixture) -> None: async def test_setup_failed_auth(hass: HomeAssistant, ufp: MockUFPFixture) -> None:
"""Test setup of unifiprotect entry with unauthorized error after multiple retries.""" """Test setup of unifiprotect entry with unauthorized error after multiple retries."""
ufp.api.get_nvr = AsyncMock(side_effect=NotAuthorized) ufp.api.get_bootstrap = AsyncMock(side_effect=NotAuthorized)
await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
assert ufp.entry.state is ConfigEntryState.SETUP_RETRY assert ufp.entry.state is ConfigEntryState.SETUP_RETRY

View file

@ -216,6 +216,7 @@ async def test_browse_media_root_multiple_consoles(
api2.api_path = "/api" api2.api_path = "/api"
api2.base_url = "https://127.0.0.2" api2.base_url = "https://127.0.0.2"
api2.connection_host = IPv4Address("127.0.0.2") api2.connection_host = IPv4Address("127.0.0.2")
api2.get_bootstrap = AsyncMock(return_value=bootstrap2)
api2.get_nvr = AsyncMock(return_value=bootstrap2.nvr) api2.get_nvr = AsyncMock(return_value=bootstrap2.nvr)
api2.update = AsyncMock(return_value=bootstrap2) api2.update = AsyncMock(return_value=bootstrap2)
api2.async_disconnect_ws = AsyncMock() api2.async_disconnect_ws = AsyncMock()

View file

@ -5,7 +5,7 @@ from copy import copy
from http import HTTPStatus from http import HTTPStatus
from unittest.mock import Mock from unittest.mock import Mock
from pyunifiprotect.data import Version from pyunifiprotect.data import CloudAccount, Version
from homeassistant.components.repairs.issue_handler import ( from homeassistant.components.repairs.issue_handler import (
async_process_repairs_platforms, async_process_repairs_platforms,
@ -15,6 +15,7 @@ from homeassistant.components.repairs.websocket_api import (
RepairsFlowResourceView, RepairsFlowResourceView,
) )
from homeassistant.components.unifiprotect.const import DOMAIN from homeassistant.components.unifiprotect.const import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .utils import MockUFPFixture, init_entry from .utils import MockUFPFixture, init_entry
@ -54,7 +55,10 @@ async def test_ea_warning_ignore(
data = await resp.json() data = await resp.json()
flow_id = data["flow_id"] flow_id = data["flow_id"]
assert data["description_placeholders"] == {"version": str(version)} assert data["description_placeholders"] == {
"learn_more": "https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access",
"version": str(version),
}
assert data["step_id"] == "start" assert data["step_id"] == "start"
url = RepairsFlowResourceView.url.format(flow_id=flow_id) url = RepairsFlowResourceView.url.format(flow_id=flow_id)
@ -63,7 +67,10 @@ async def test_ea_warning_ignore(
data = await resp.json() data = await resp.json()
flow_id = data["flow_id"] flow_id = data["flow_id"]
assert data["description_placeholders"] == {"version": str(version)} assert data["description_placeholders"] == {
"learn_more": "https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access",
"version": str(version),
}
assert data["step_id"] == "confirm" assert data["step_id"] == "confirm"
url = RepairsFlowResourceView.url.format(flow_id=flow_id) url = RepairsFlowResourceView.url.format(flow_id=flow_id)
@ -106,7 +113,10 @@ async def test_ea_warning_fix(
data = await resp.json() data = await resp.json()
flow_id = data["flow_id"] flow_id = data["flow_id"]
assert data["description_placeholders"] == {"version": str(version)} assert data["description_placeholders"] == {
"learn_more": "https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access",
"version": str(version),
}
assert data["step_id"] == "start" assert data["step_id"] == "start"
new_nvr = copy(ufp.api.bootstrap.nvr) new_nvr = copy(ufp.api.bootstrap.nvr)
@ -125,3 +135,50 @@ async def test_ea_warning_fix(
data = await resp.json() data = await resp.json()
assert data["type"] == "create_entry" assert data["type"] == "create_entry"
async def test_cloud_user_fix(
hass: HomeAssistant,
ufp: MockUFPFixture,
cloud_account: CloudAccount,
hass_client: ClientSessionGenerator,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test EA warning is created if using prerelease version of Protect."""
ufp.api.bootstrap.nvr.version = Version("2.2.6")
user = ufp.api.bootstrap.users[ufp.api.bootstrap.auth_user_id]
user.cloud_account = cloud_account
ufp.api.bootstrap.users[ufp.api.bootstrap.auth_user_id] = user
await init_entry(hass, ufp, [])
await async_process_repairs_platforms(hass)
ws_client = await hass_ws_client(hass)
client = await hass_client()
await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await ws_client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) > 0
issue = None
for i in msg["result"]["issues"]:
if i["issue_id"] == "cloud_user":
issue = i
assert issue is not None
url = RepairsFlowIndexView.url
resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "cloud_user"})
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data["flow_id"]
assert data["step_id"] == "confirm"
url = RepairsFlowResourceView.url.format(flow_id=flow_id)
resp = await client.post(url)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data["type"] == "create_entry"
await hass.async_block_till_done()
assert any(ufp.entry.async_get_active_flows(hass, {SOURCE_REAUTH}))