From c96d4991b9a854a09a0573709d56cc56696b18af Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 24 Sep 2024 11:46:43 +0200 Subject: [PATCH] Add issue asking users to disable VoIP call_in_progress binary sensor (#126504) * Add issue asking users to disable VoIP call_in_progress binary sensor * Add tests * Add files * Update homeassistant/components/voip/binary_sensor.py Co-authored-by: Joost Lekkerkerker * Fix test --------- Co-authored-by: Joost Lekkerkerker --- .../components/assist_pipeline/strings.json | 4 +- .../components/voip/binary_sensor.py | 33 +++++ homeassistant/components/voip/repairs.py | 22 ++++ homeassistant/components/voip/strings.json | 12 ++ tests/components/voip/test_binary_sensor.py | 120 +++++++++++++++++- tests/components/voip/test_repairs.py | 13 ++ 6 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/voip/repairs.py create mode 100644 tests/components/voip/test_repairs.py diff --git a/homeassistant/components/assist_pipeline/strings.json b/homeassistant/components/assist_pipeline/strings.json index d81bcf83a1a..956c17dad60 100644 --- a/homeassistant/components/assist_pipeline/strings.json +++ b/homeassistant/components/assist_pipeline/strings.json @@ -24,11 +24,11 @@ }, "issues": { "assist_in_progress_deprecated": { - "title": "{integration_name} assist in progress binary sensors are deprecated", + "title": "{integration_name} in progress binary sensors are deprecated", "fix_flow": { "step": { "confirm_disable_entity": { - "description": "The {integration_name} assist in progress binary sensor `{entity_id}` is deprecated.\n\nMigrate your configuration to use the corresponding `{assist_satellite_domain}` entity and then click SUBMIT to disable the assist in progress binary sensor and fix this issue." + "description": "The {integration_name} in progress binary sensor `{entity_id}` is deprecated.\n\nMigrate your configuration to use the corresponding `{assist_satellite_domain}` entity and then click SUBMIT to disable the in progress binary sensor and fix this issue." } } } diff --git a/homeassistant/components/voip/binary_sensor.py b/homeassistant/components/voip/binary_sensor.py index a1ef36a7086..f38b228c46c 100644 --- a/homeassistant/components/voip/binary_sensor.py +++ b/homeassistant/components/voip/binary_sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -56,6 +57,38 @@ class VoIPCallInProgress(VoIPEntity, BinarySensorEntity): self.voip_device.async_listen_update(self._is_active_changed) ) + await super().async_added_to_hass() + if TYPE_CHECKING: + assert self.registry_entry is not None + ir.async_create_issue( + self.hass, + DOMAIN, + f"assist_in_progress_deprecated_{self.registry_entry.id}", + breaks_in_ha_version="2025.4", + data={ + "entity_id": self.entity_id, + "entity_uuid": self.registry_entry.id, + "integration_name": "VoIP", + }, + is_fixable=True, + severity=ir.IssueSeverity.WARNING, + translation_key="assist_in_progress_deprecated", + translation_placeholders={ + "integration_name": "VoIP", + }, + ) + + async def async_will_remove_from_hass(self) -> None: + """Remove issue.""" + await super().async_will_remove_from_hass() + if TYPE_CHECKING: + assert self.registry_entry is not None + ir.async_delete_issue( + self.hass, + DOMAIN, + f"assist_in_progress_deprecated_{self.registry_entry.id}", + ) + @callback def _is_active_changed(self, device: VoIPDevice) -> None: """Call when active state changed.""" diff --git a/homeassistant/components/voip/repairs.py b/homeassistant/components/voip/repairs.py new file mode 100644 index 00000000000..11cacbb7486 --- /dev/null +++ b/homeassistant/components/voip/repairs.py @@ -0,0 +1,22 @@ +"""Repairs implementation for the VoIP integration.""" + +from __future__ import annotations + +from homeassistant.components.assist_pipeline.repair_flows import ( + AssistInProgressDeprecatedRepairFlow, +) +from homeassistant.components.repairs import RepairsFlow +from homeassistant.core import HomeAssistant + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: + """Create flow.""" + if issue_id.startswith("assist_in_progress_deprecated"): + return AssistInProgressDeprecatedRepairFlow(data) + # If VoIP adds confirm-only repairs in the future, this should be changed + # to return a ConfirmRepairFlow instead of raising a ValueError + raise ValueError(f"unknown repair {issue_id}") diff --git a/homeassistant/components/voip/strings.json b/homeassistant/components/voip/strings.json index 750f526ba1b..9da7cf7d534 100644 --- a/homeassistant/components/voip/strings.json +++ b/homeassistant/components/voip/strings.json @@ -47,6 +47,18 @@ } } }, + "issues": { + "assist_in_progress_deprecated": { + "title": "[%key:component::assist_pipeline::issues::assist_in_progress_deprecated::title%]", + "fix_flow": { + "step": { + "confirm_disable_entity": { + "description": "[%key:component::assist_pipeline::issues::assist_in_progress_deprecated::fix_flow::step::confirm_disable_entity::description%]" + } + } + } + } + }, "options": { "step": { "init": { diff --git a/tests/components/voip/test_binary_sensor.py b/tests/components/voip/test_binary_sensor.py index 50a8c5d4141..44ac8e4d77f 100644 --- a/tests/components/voip/test_binary_sensor.py +++ b/tests/components/voip/test_binary_sensor.py @@ -1,11 +1,18 @@ """Test VoIP binary sensor devices.""" +from http import HTTPStatus + import pytest +from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN +from homeassistant.components.voip import DOMAIN from homeassistant.components.voip.devices import VoIPDevice from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import entity_registry as er, issue_registry as ir +from homeassistant.setup import async_setup_component + +from tests.typing import ClientSessionGenerator @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -45,3 +52,114 @@ async def test_assist_in_progress_disabled_by_default( assert entity_entry assert entity_entry.disabled assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_assist_in_progress_issue( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + issue_registry: ir.IssueRegistry, + voip_device: VoIPDevice, +) -> None: + """Test assist in progress binary sensor.""" + + call_in_progress_entity_id = "binary_sensor.192_168_1_210_call_in_progress" + + state = hass.states.get(call_in_progress_entity_id) + assert state is not None + + entity_entry = entity_registry.async_get(call_in_progress_entity_id) + issue = issue_registry.async_get_issue( + DOMAIN, f"assist_in_progress_deprecated_{entity_entry.id}" + ) + assert issue is not None + + # Test issue goes away after disabling the entity + entity_registry.async_update_entity( + call_in_progress_entity_id, + disabled_by=er.RegistryEntryDisabler.USER, + ) + await hass.async_block_till_done() + issue = issue_registry.async_get_issue( + DOMAIN, f"assist_in_progress_deprecated_{entity_entry.id}" + ) + assert issue is None + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_assist_in_progress_repair_flow( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + entity_registry: er.EntityRegistry, + issue_registry: ir.IssueRegistry, + voip_device: VoIPDevice, +) -> None: + """Test assist in progress binary sensor deprecation issue flow.""" + + call_in_progress_entity_id = "binary_sensor.192_168_1_210_call_in_progress" + + state = hass.states.get(call_in_progress_entity_id) + assert state is not None + + entity_entry = entity_registry.async_get(call_in_progress_entity_id) + assert entity_entry.disabled_by is None + issue = issue_registry.async_get_issue( + DOMAIN, f"assist_in_progress_deprecated_{entity_entry.id}" + ) + assert issue is not None + assert issue.data == { + "entity_id": call_in_progress_entity_id, + "entity_uuid": entity_entry.id, + "integration_name": "VoIP", + } + assert issue.translation_key == "assist_in_progress_deprecated" + assert issue.translation_placeholders == {"integration_name": "VoIP"} + + assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) + await hass.async_block_till_done() + await hass.async_start() + + client = await hass_client() + + resp = await client.post( + "/api/repairs/issues/fix", + json={"handler": DOMAIN, "issue_id": issue.issue_id}, + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "data_schema": [], + "description_placeholders": { + "assist_satellite_domain": "assist_satellite", + "entity_id": call_in_progress_entity_id, + "integration_name": "VoIP", + }, + "errors": None, + "flow_id": flow_id, + "handler": DOMAIN, + "last_step": None, + "preview": None, + "step_id": "confirm_disable_entity", + "type": "form", + } + + resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "description": None, + "description_placeholders": None, + "flow_id": flow_id, + "handler": DOMAIN, + "type": "create_entry", + } + + # Test the entity is disabled + entity_entry = entity_registry.async_get(call_in_progress_entity_id) + assert entity_entry.disabled_by is er.RegistryEntryDisabler.USER diff --git a/tests/components/voip/test_repairs.py b/tests/components/voip/test_repairs.py new file mode 100644 index 00000000000..ec2a2cfed96 --- /dev/null +++ b/tests/components/voip/test_repairs.py @@ -0,0 +1,13 @@ +"""Test VoIP repairs.""" + +import pytest + +from homeassistant.components.voip import repairs +from homeassistant.core import HomeAssistant + + +async def test_create_fix_flow_raises_on_unknown_issue_id(hass: HomeAssistant) -> None: + """Test reate_fix_flow raises on unknown issue_id.""" + + with pytest.raises(ValueError): + await repairs.async_create_fix_flow(hass, "no_such_issue", None)