diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 4e659d39cc5..1e9f168820c 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -27,6 +27,7 @@ from .const import ( from .data import ProtectData, async_ufp_instance_for_config_entry_ids from .discovery import async_start_discovery from .migrate import async_migrate_data +from .repairs import async_create_repairs from .services import async_cleanup_services, async_setup_services from .utils import ( _async_unifi_mac_from_hass, @@ -121,6 +122,7 @@ async def _async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, data_service: ProtectData ) -> None: await async_migrate_data(hass, entry, data_service.api) + await async_create_repairs(hass, entry, data_service.api) await data_service.async_setup() if not data_service.last_update_success: diff --git a/homeassistant/components/unifiprotect/repairs.py b/homeassistant/components/unifiprotect/repairs.py index dd1aaa283dc..72f297c8c25 100644 --- a/homeassistant/components/unifiprotect/repairs.py +++ b/homeassistant/components/unifiprotect/repairs.py @@ -2,20 +2,93 @@ from __future__ import annotations -from typing import cast +from functools import partial +from itertools import chain +import logging +from typing import Any, cast from pyunifiprotect import ProtectApiClient import voluptuous as vol from homeassistant import data_entry_flow +from homeassistant.components.automation import ( + EVENT_AUTOMATION_RELOADED, + automations_with_entity, +) from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow +from homeassistant.components.script import scripts_with_entity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.issue_registry import async_get as async_get_issue_registry +from homeassistant.helpers import entity_registry as er, issue_registry as ir +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_get as async_get_issue_registry, +) -from .const import CONF_ALLOW_EA +from .const import CONF_ALLOW_EA, DOMAIN from .utils import async_create_api_client +_LOGGER = logging.getLogger(__name__) + + +async def async_create_repairs( + hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient +) -> None: + """Create any additional repairs for deprecations.""" + + await _deprecate_smart_sensor(hass, entry, protect) + entry.async_on_unload( + hass.bus.async_listen( + EVENT_AUTOMATION_RELOADED, + partial(_deprecate_smart_sensor, hass, entry, protect), + ) + ) + + +async def _deprecate_smart_sensor( + hass: HomeAssistant, + entry: ConfigEntry, + protect: ProtectApiClient, + *args: Any, + **kwargs: Any, +) -> None: + entity_registry = er.async_get(hass) + automations: dict[str, list[str]] = {} + scripts: dict[str, list[str]] = {} + for entity in er.async_entries_for_config_entry(entity_registry, entry.entry_id): + if ( + entity.domain == Platform.SENSOR + and entity.disabled_by is None + and "detected_object" in entity.unique_id + ): + entity_automations = automations_with_entity(hass, entity.entity_id) + entity_scripts = scripts_with_entity(hass, entity.entity_id) + if entity_automations: + automations[entity.entity_id] = entity_automations + if entity_scripts: + scripts[entity.entity_id] = entity_scripts + + if automations or scripts: + items = sorted( + set( + chain.from_iterable(list(automations.values()) + list(scripts.values())) + ) + ) + ir.async_create_issue( + hass, + DOMAIN, + "deprecate_smart_sensor", + is_fixable=False, + breaks_in_ha_version="2023.3.0", + severity=IssueSeverity.WARNING, + translation_key="deprecate_smart_sensor", + translation_placeholders={"items": "* `" + "`\n* `".join(items) + "`\n"}, + ) + else: + _LOGGER.debug("No found usages of Detected Object sensor") + ir.async_delete_issue(hass, DOMAIN, "deprecate_smart_sensor") + class EAConfirm(RepairsFlow): """Handler for an issue fixing flow.""" diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index 6c0639eaa56..2c0b894746e 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -76,6 +76,10 @@ "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}" }, + "deprecate_smart_sensor": { + "title": "Smart Detection Sensor Deprecated", + "description": "The unified \"Detected Object\" sensor for smart detections is now deprecated. It has been replaced with individual smart detection binary sensors for each smart detection type.\n\nBelow are the detected automations or scripts that use one or more of the deprecated entities:\n{items}\nThe above list may be incomplete and it does not include any template usages inside of dashboards. Please update any templates, automations or scripts accordingly." + }, "deprecated_service_set_doorbell_message": { "title": "set_doorbell_message is Deprecated", "fix_flow": { diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index 0c6bf34d703..d19bf73bab8 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -51,6 +51,10 @@ } }, "issues": { + "deprecate_smart_sensor": { + "description": "The unified \"Detected Object\" sensor for smart detections is now deprecated. It has been replaced with individual smart detection binary sensors for each smart detection type.\n\nBelow are the detected automations or scripts that use one or more of the deprecated entities:\n{items}\nThe above list may be incomplete and it does not include any template usages inside of dashboards. Please update any templates, automations or scripts accordingly.", + "title": "Smart Detection Sensor Deprecated" + }, "deprecated_service_set_doorbell_message": { "fix_flow": { "step": { diff --git a/tests/components/unifiprotect/test_repairs.py b/tests/components/unifiprotect/test_repairs.py index 3ffd2ea4a43..1d1d7315aac 100644 --- a/tests/components/unifiprotect/test_repairs.py +++ b/tests/components/unifiprotect/test_repairs.py @@ -4,10 +4,11 @@ from __future__ import annotations from copy import copy from http import HTTPStatus -from unittest.mock import Mock +from unittest.mock import Mock, patch -from pyunifiprotect.data import Version +from pyunifiprotect.data import Camera, Version +from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN from homeassistant.components.repairs.issue_handler import ( async_process_repairs_platforms, ) @@ -15,8 +16,12 @@ from homeassistant.components.repairs.websocket_api import ( RepairsFlowIndexView, RepairsFlowResourceView, ) +from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN from homeassistant.components.unifiprotect.const import DOMAIN +from homeassistant.const import SERVICE_RELOAD, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component from .utils import MockUFPFixture, init_entry @@ -124,3 +129,198 @@ async def test_ea_warning_fix( data = await resp.json() assert data["type"] == "create_entry" + + +async def test_deprecate_smart_default( + hass: HomeAssistant, ufp: MockUFPFixture, hass_ws_client, doorbell: Camera +): + """Test Deprecate Sensor repair does not exist by default (new installs).""" + + await init_entry(hass, ufp, [doorbell]) + + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "deprecate_smart_sensor": + issue = i + assert issue is None + + +async def test_deprecate_smart_no_automations( + hass: HomeAssistant, ufp: MockUFPFixture, hass_ws_client, doorbell: Camera +): + """Test Deprecate Sensor repair exists for existing installs.""" + + registry = er.async_get(hass) + registry.async_get_or_create( + Platform.SENSOR, + DOMAIN, + f"{doorbell.mac}_detected_object", + config_entry=ufp.entry, + ) + + await init_entry(hass, ufp, [doorbell]) + + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "deprecate_smart_sensor": + issue = i + assert issue is None + + +async def _load_automation(hass: HomeAssistant, entity_id: str): + assert await async_setup_component( + hass, + AUTOMATION_DOMAIN, + { + AUTOMATION_DOMAIN: [ + { + "alias": "test1", + "trigger": [ + {"platform": "state", "entity_id": entity_id}, + { + "platform": "event", + "event_type": "state_changed", + "event_data": {"entity_id": entity_id}, + }, + ], + "condition": { + "condition": "state", + "entity_id": entity_id, + "state": "on", + }, + "action": [ + { + "service": "test.script", + "data": {"entity_id": entity_id}, + }, + ], + }, + ] + }, + ) + + +async def test_deprecate_smart_automation( + hass: HomeAssistant, ufp: MockUFPFixture, hass_ws_client, doorbell: Camera +): + """Test Deprecate Sensor repair exists for existing installs.""" + + registry = er.async_get(hass) + entry = registry.async_get_or_create( + Platform.SENSOR, + DOMAIN, + f"{doorbell.mac}_detected_object", + config_entry=ufp.entry, + ) + await _load_automation(hass, entry.entity_id) + await init_entry(hass, ufp, [doorbell]) + + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "deprecate_smart_sensor": + issue = i + assert issue is not None + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={AUTOMATION_DOMAIN: []}, + ): + await hass.services.async_call(AUTOMATION_DOMAIN, SERVICE_RELOAD, blocking=True) + await hass.async_block_till_done() + + await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "deprecate_smart_sensor": + issue = i + assert issue is None + + +async def _load_script(hass: HomeAssistant, entity_id: str): + assert await async_setup_component( + hass, + SCRIPT_DOMAIN, + { + SCRIPT_DOMAIN: { + "test": { + "sequence": { + "service": "test.script", + "data": {"entity_id": entity_id}, + } + } + }, + }, + ) + + +async def test_deprecate_smart_script( + hass: HomeAssistant, ufp: MockUFPFixture, hass_ws_client, doorbell: Camera +): + """Test Deprecate Sensor repair exists for existing installs.""" + + registry = er.async_get(hass) + entry = registry.async_get_or_create( + Platform.SENSOR, + DOMAIN, + f"{doorbell.mac}_detected_object", + config_entry=ufp.entry, + ) + await _load_script(hass, entry.entity_id) + await init_entry(hass, ufp, [doorbell]) + + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "deprecate_smart_sensor": + issue = i + assert issue is not None + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={SCRIPT_DOMAIN: {}}, + ): + await hass.services.async_call(SCRIPT_DOMAIN, SERVICE_RELOAD, blocking=True) + await hass.config_entries.async_reload(ufp.entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "deprecate_smart_sensor": + issue = i + assert issue is None