diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 174c5476cff..4e659d39cc5 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -5,31 +5,19 @@ import asyncio from datetime import timedelta import logging -from aiohttp import CookieJar from aiohttp.client_exceptions import ServerDisconnectedError -from pyunifiprotect import ProtectApiClient from pyunifiprotect.exceptions import ClientError, NotAuthorized from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, issue_registry as ir -from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.issue_registry import IssueSeverity from .const import ( - CONF_ALL_UPDATES, - CONF_OVERRIDE_CHOST, + CONF_ALLOW_EA, DEFAULT_SCAN_INTERVAL, - DEVICES_FOR_SUBSCRIBE, DEVICES_THAT_ADOPT, DOMAIN, MIN_REQUIRED_PROTECT_V, @@ -40,7 +28,11 @@ from .data import ProtectData, async_ufp_instance_for_config_entry_ids from .discovery import async_start_discovery from .migrate import async_migrate_data from .services import async_cleanup_services, async_setup_services -from .utils import _async_unifi_mac_from_hass, async_get_devices +from .utils import ( + _async_unifi_mac_from_hass, + async_create_api_client, + async_get_devices, +) from .views import ThumbnailProxyView, VideoProxyView _LOGGER = logging.getLogger(__name__) @@ -52,19 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the UniFi Protect config entries.""" async_start_discovery(hass) - session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) - protect = ProtectApiClient( - host=entry.data[CONF_HOST], - port=entry.data[CONF_PORT], - username=entry.data[CONF_USERNAME], - password=entry.data[CONF_PASSWORD], - verify_ssl=entry.data[CONF_VERIFY_SSL], - session=session, - subscribed_models=DEVICES_FOR_SUBSCRIBE, - override_connection_host=entry.options.get(CONF_OVERRIDE_CHOST, False), - ignore_stats=not entry.options.get(CONF_ALL_UPDATES, False), - ignore_unadopted=False, - ) + protect = async_create_api_client(hass, entry) _LOGGER.debug("Connect to UniFi Protect") data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry) @@ -83,42 +63,75 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - await async_migrate_data(hass, entry, protect) if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=nvr_info.mac) - await data_service.async_setup() - if not data_service.last_update_success: - raise ConfigEntryNotReady - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_service - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - async_setup_services(hass) - hass.http.register_view(ThumbnailProxyView(hass)) - hass.http.register_view(VideoProxyView(hass)) - entry.async_on_unload(entry.add_update_listener(_async_options_updated)) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop) ) - if await data_service.api.bootstrap.get_is_prerelease(): - protect_version = data_service.api.bootstrap.nvr.version + if ( + not entry.options.get(CONF_ALLOW_EA, False) + and await nvr_info.get_is_prerelease() + ): ir.async_create_issue( hass, DOMAIN, - f"ea_warning_{protect_version}", - is_fixable=False, - is_persistent=False, + "ea_warning", + is_fixable=True, + is_persistent=True, learn_more_url="https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access", severity=IssueSeverity.WARNING, translation_key="ea_warning", - translation_placeholders={"version": str(protect_version)}, + translation_placeholders={"version": str(nvr_info.version)}, + data={"entry_id": entry.entry_id}, ) + try: + await _async_setup_entry(hass, entry, data_service) + except Exception as err: + if await nvr_info.get_is_prerelease(): + # If they are running a pre-release, its quite common for setup + # to fail so we want to create a repair issue for them so its + # obvious what the problem is. + ir.async_create_issue( + hass, + DOMAIN, + f"ea_setup_failed_{nvr_info.version}", + is_fixable=False, + is_persistent=False, + learn_more_url="https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access", + severity=IssueSeverity.ERROR, + translation_key="ea_setup_failed", + translation_placeholders={ + "error": str(err), + "version": str(nvr_info.version), + }, + ) + ir.async_delete_issue(hass, DOMAIN, "ea_warning") + _LOGGER.exception("Error setting up UniFi Protect integration: %s", err) + raise + return True +async def _async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, data_service: ProtectData +) -> None: + await async_migrate_data(hass, entry, data_service.api) + + await data_service.async_setup() + if not data_service.last_update_success: + raise ConfigEntryNotReady + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + async_setup_services(hass) + hass.http.register_view(ThumbnailProxyView(hass)) + hass.http.register_view(VideoProxyView(hass)) + + async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" await hass.config_entries.async_reload(entry.entry_id) @@ -126,6 +139,7 @@ async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> Non async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload UniFi Protect config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): data: ProtectData = hass.data[DOMAIN][entry.entry_id] await data.async_stop() diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index f07ca923a53..571922d8651 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -34,6 +34,7 @@ from homeassistant.util.network import is_ip_address from .const import ( CONF_ALL_UPDATES, + CONF_ALLOW_EA, CONF_DISABLE_RTSP, CONF_MAX_MEDIA, CONF_OVERRIDE_CHOST, @@ -224,6 +225,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_ALL_UPDATES: False, CONF_OVERRIDE_CHOST: False, CONF_MAX_MEDIA: DEFAULT_MAX_MEDIA, + CONF_ALLOW_EA: False, }, ) @@ -392,6 +394,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA ), ): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)), + vol.Optional( + CONF_ALLOW_EA, + default=self.config_entry.options.get(CONF_ALLOW_EA, False), + ): bool, } ), ) diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 93a0fa5ff74..f751ed6a009 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -20,6 +20,7 @@ CONF_DISABLE_RTSP = "disable_rtsp" CONF_ALL_UPDATES = "all_updates" CONF_OVERRIDE_CHOST = "override_connection_host" CONF_MAX_MEDIA = "max_media" +CONF_ALLOW_EA = "allow_ea" CONFIG_OPTIONS = [ CONF_ALL_UPDATES, diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 7a412fede1d..d9d7b76db41 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -5,7 +5,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", "requirements": ["pyunifiprotect==4.4.1", "unifi-discovery==1.1.7"], - "dependencies": ["http"], + "dependencies": ["http", "repairs"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/homeassistant/components/unifiprotect/repairs.py b/homeassistant/components/unifiprotect/repairs.py new file mode 100644 index 00000000000..98846113e5e --- /dev/null +++ b/homeassistant/components/unifiprotect/repairs.py @@ -0,0 +1,96 @@ +"""unifiprotect.repairs.""" + +from __future__ import annotations + +from typing import cast + +from pyunifiprotect import ProtectApiClient +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.issue_registry import async_get as async_get_issue_registry + +from .const import CONF_ALLOW_EA +from .utils import async_create_api_client + + +class EAConfirm(RepairsFlow): + """Handler for an issue fixing flow.""" + + _api: ProtectApiClient + _entry: ConfigEntry + + def __init__(self, api: ProtectApiClient, entry: ConfigEntry) -> None: + """Create flow.""" + + self._api = api + self._entry = entry + super().__init__() + + @callback + def _async_get_placeholders(self) -> dict[str, str] | None: + issue_registry = async_get_issue_registry(self.hass) + description_placeholders = None + if issue := issue_registry.async_get_issue(self.handler, self.issue_id): + description_placeholders = issue.translation_placeholders + + return description_placeholders + + 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_start() + + async def async_step_start( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is None: + placeholders = self._async_get_placeholders() + return self.async_show_form( + step_id="start", + data_schema=vol.Schema({}), + description_placeholders=placeholders, + ) + + nvr = await self._api.get_nvr() + if await nvr.get_is_prerelease(): + return await self.async_step_confirm() + await self.hass.config_entries.async_reload(self._entry.entry_id) + return self.async_create_entry(title="", data={}) + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + options = dict(self._entry.options) + options[CONF_ALLOW_EA] = True + self.hass.config_entries.async_update_entry(self._entry, options=options) + return self.async_create_entry(title="", data={}) + + placeholders = self._async_get_placeholders() + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema({}), + description_placeholders=placeholders, + ) + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: + """Create flow.""" + if data is not None and issue_id == "ea_warning": + 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 EAConfirm(api, entry) + return ConfirmRepairFlow() diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index 5c9416440de..abac7701279 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -50,7 +50,8 @@ "disable_rtsp": "Disable the RTSP stream", "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "override_connection_host": "Override Connection Host", - "max_media": "Max number of event to load for Media Browser (increases RAM usage)" + "max_media": "Max number of event to load for Media Browser (increases RAM usage)", + "allow_ea": "Allow Early Access versions of Protect (WARNING: Will mark your integration as unsupported)" } } } @@ -58,7 +59,22 @@ "issues": { "ea_warning": { "title": "UniFi Protect v{version} is an Early Access version", - "description": "You are using v{version} of UniFi Protect which is an Early Access version. Early Access versions are not supported by Home Assistant and may cause your UniFi Protect integration to break or not work as expected." + "fix_flow": { + "step": { + "start": { + "title": "v{version} is an Early Access version", + "description": "You are using v{version} of UniFi Protect which is an Early Access version. [Early Access versions are not supported by Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) and it is recommended to go back to a stable release as soon as possible.\n\nBy submitting this form you have either [downgraded UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) or you agree to run an unsupported version of UniFi Protect." + }, + "confirm": { + "title": "v{version} is an Early Access version", + "description": "Are you sure you want to run unsupported versions of UniFi Protect? This may cause your Home Assistant integration to break." + } + } + } + }, + "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}" } } } diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index 742a1b3a851..65a398375fe 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -42,21 +42,33 @@ } }, "issues": { + "ea_setup_failed": { + "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}", + "title": "Setup error using Early Access version" + }, "ea_warning": { - "description": "You are using v{version} of UniFi Protect which is an Early Access version. Early Access versions are not supported by Home Assistant and may cause your UniFi Protect integration to break or not work as expected.", + "fix_flow": { + "step": { + "confirm": { + "description": "Are you sure you want to run unsupported versions of UniFi Protect? This may cause your Home Assistant integration to break.", + "title": "v{version} is an Early Access version" + }, + "start": { + "description": "You are using v{version} of UniFi Protect which is an Early Access version. [Early Access versions are not supported by Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) and it is recommended to go back to a stable release as soon as possible.\n\nBy submitting this form you have either [downgraded UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) or you agree to run an unsupported version of UniFi Protect.", + "title": "v{version} is an Early Access version" + } + } + }, "title": "UniFi Protect v{version} is an Early Access version" } }, "options": { - "error": { - "invalid_mac_list": "Must be a list of MAC addresses seperated by commas" - }, "step": { "init": { "data": { "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", + "allow_ea": "Allow Early Access versions of Protect (WARNING: Will mark your integration as unsupported)", "disable_rtsp": "Disable the RTSP stream", - "ignored_devices": "Comma separated list of MAC addresses of devices to ignore", "max_media": "Max number of event to load for Media Browser (increases RAM usage)", "override_connection_host": "Override Connection Host" }, diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 808117aac9e..f58bb14eb41 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -7,6 +7,8 @@ from enum import Enum import socket from typing import Any +from aiohttp import CookieJar +from pyunifiprotect import ProtectApiClient from pyunifiprotect.data import ( Bootstrap, Light, @@ -16,9 +18,23 @@ from pyunifiprotect.data import ( ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .const import DOMAIN, ModelType +from .const import ( + CONF_ALL_UPDATES, + CONF_OVERRIDE_CHOST, + DEVICES_FOR_SUBSCRIBE, + DOMAIN, + ModelType, +) def get_nested_attr(obj: Any, attr: str) -> Any: @@ -106,3 +122,24 @@ def async_dispatch_id(entry: ConfigEntry, dispatch: str) -> str: """Generate entry specific dispatch ID.""" return f"{DOMAIN}.{entry.entry_id}.{dispatch}" + + +@callback +def async_create_api_client( + hass: HomeAssistant, entry: ConfigEntry +) -> ProtectApiClient: + """Create ProtectApiClient from config entry.""" + + session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) + return ProtectApiClient( + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + verify_ssl=entry.data[CONF_VERIFY_SSL], + session=session, + subscribed_models=DEVICES_FOR_SUBSCRIBE, + override_connection_host=entry.options.get(CONF_OVERRIDE_CHOST, False), + ignore_stats=not entry.options.get(CONF_ALL_UPDATES, False), + ignore_unadopted=False, + ) diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index b006dfbd004..77aa9622f9e 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -128,7 +128,7 @@ def mock_entry( """Mock ProtectApiClient for testing.""" with _patch_discovery(no_device=True), patch( - "homeassistant.components.unifiprotect.ProtectApiClient" + "homeassistant.components.unifiprotect.utils.ProtectApiClient" ) as mock_api: ufp_config_entry.add_to_hass(hass) diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index dc91b0f6dde..1ee1d40515b 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -246,7 +246,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - mock_config.add_to_hass(hass) with _patch_discovery(), patch( - "homeassistant.components.unifiprotect.ProtectApiClient" + "homeassistant.components.unifiprotect.utils.ProtectApiClient" ) as mock_api: mock_api.return_value = ufp_client @@ -274,6 +274,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - "disable_rtsp": True, "override_connection_host": True, "max_media": 1000, + "allow_ea": False, } await hass.config_entries.async_unload(mock_config.entry_id) diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 9392caa30ac..04b4928aaec 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -72,7 +72,9 @@ async def test_setup_multiple( nvr.id ufp.api.get_nvr = AsyncMock(return_value=nvr) - with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: + with patch( + "homeassistant.components.unifiprotect.utils.ProtectApiClient" + ) as mock_api: mock_config = MockConfigEntry( domain=DOMAIN, data={ @@ -194,7 +196,7 @@ async def test_setup_starts_discovery( ): """Test setting up will start discovery.""" with _patch_discovery(), patch( - "homeassistant.components.unifiprotect.ProtectApiClient" + "homeassistant.components.unifiprotect.utils.ProtectApiClient" ) as mock_api: ufp_config_entry.add_to_hass(hass) mock_api.return_value = ufp_client diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index 8200a1323ab..3447cdfc139 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -222,7 +222,9 @@ async def test_browse_media_root_multiple_consoles( api2.update = AsyncMock(return_value=bootstrap2) api2.async_disconnect_ws = AsyncMock() - with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: + with patch( + "homeassistant.components.unifiprotect.utils.ProtectApiClient" + ) as mock_api: mock_config = MockConfigEntry( domain=DOMAIN, data={ @@ -285,7 +287,9 @@ async def test_browse_media_root_multiple_consoles_only_one_media( api2.update = AsyncMock(return_value=bootstrap2) api2.async_disconnect_ws = AsyncMock() - with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: + with patch( + "homeassistant.components.unifiprotect.utils.ProtectApiClient" + ) as mock_api: mock_config = MockConfigEntry( domain=DOMAIN, data={ diff --git a/tests/components/unifiprotect/test_repairs.py b/tests/components/unifiprotect/test_repairs.py new file mode 100644 index 00000000000..f6f677a1976 --- /dev/null +++ b/tests/components/unifiprotect/test_repairs.py @@ -0,0 +1,120 @@ +"""Test repairs for unifiprotect.""" + +from __future__ import annotations + +from copy import copy +from http import HTTPStatus +from unittest.mock import Mock + +from pyunifiprotect.data import Version + +from homeassistant.components.repairs.issue_handler import ( + async_process_repairs_platforms, +) +from homeassistant.components.repairs.websocket_api import ( + RepairsFlowIndexView, + RepairsFlowResourceView, +) +from homeassistant.components.unifiprotect.const import DOMAIN +from homeassistant.core import HomeAssistant + +from .utils import MockUFPFixture, init_entry + + +async def test_ea_warning_ignore( + hass: HomeAssistant, + ufp: MockUFPFixture, + hass_client, + hass_ws_client, +): + """Test EA warning is created if using prerelease version of Protect.""" + + version = ufp.api.bootstrap.nvr.version + assert version.is_prerelease + 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"]) == 1 + issue = msg["result"]["issues"][0] + assert issue["issue_id"] == "ea_warning" + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["description_placeholders"] == {"version": str(version)} + assert data["step_id"] == "start" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["description_placeholders"] == {"version": str(version)} + 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" + + +async def test_ea_warning_fix( + hass: HomeAssistant, + ufp: MockUFPFixture, + hass_client, + hass_ws_client, +): + """Test EA warning is created if using prerelease version of Protect.""" + + version = ufp.api.bootstrap.nvr.version + assert version.is_prerelease + 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"]) == 1 + issue = msg["result"]["issues"][0] + assert issue["issue_id"] == "ea_warning" + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["description_placeholders"] == {"version": str(version)} + assert data["step_id"] == "start" + + new_nvr = copy(ufp.api.bootstrap.nvr) + new_nvr.version = Version("2.2.6") + mock_msg = Mock() + mock_msg.changed_data = {"version": "2.2.6"} + mock_msg.new_obj = new_nvr + + ufp.api.bootstrap.nvr = new_nvr + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + + 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"