diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index bf773fead0c..deccb6cc7da 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -2,50 +2,22 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from functools import partial from aioesphomeapi import BinarySensorInfo, BinarySensorState, EntityInfo from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, - BinarySensorEntityDescription, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import issue_registry as ir -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.core import callback from homeassistant.util.enum import try_parse_enum -from .const import DOMAIN -from .entity import EsphomeAssistEntity, EsphomeEntity, platform_async_setup_entry -from .entry_data import ESPHomeConfigEntry +from .entity import EsphomeEntity, platform_async_setup_entry PARALLEL_UPDATES = 0 -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Set up ESPHome binary sensors based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=BinarySensorInfo, - entity_type=EsphomeBinarySensor, - state_type=BinarySensorState, - ) - - entry_data = entry.runtime_data - assert entry_data.device_info is not None - if entry_data.device_info.voice_assistant_feature_flags_compat( - entry_data.api_version - ): - async_add_entities([EsphomeAssistInProgressBinarySensor(entry_data)]) - - class EsphomeBinarySensor( EsphomeEntity[BinarySensorInfo, BinarySensorState], BinarySensorEntity ): @@ -76,50 +48,9 @@ class EsphomeBinarySensor( return self._static_info.is_status_binary_sensor or super().available -class EsphomeAssistInProgressBinarySensor(EsphomeAssistEntity, BinarySensorEntity): - """A binary sensor implementation for ESPHome for use with assist_pipeline.""" - - entity_description = BinarySensorEntityDescription( - entity_registry_enabled_default=False, - key="assist_in_progress", - translation_key="assist_in_progress", - ) - - async def async_added_to_hass(self) -> None: - """Create issue.""" - 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": "ESPHome", - }, - is_fixable=True, - severity=ir.IssueSeverity.WARNING, - translation_key="assist_in_progress_deprecated", - translation_placeholders={ - "integration_name": "ESPHome", - }, - ) - - 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}", - ) - - @property - def is_on(self) -> bool | None: - """Return true if the binary sensor is on.""" - return self._entry_data.assist_pipeline_state +async_setup_entry = partial( + platform_async_setup_entry, + info_type=BinarySensorInfo, + entity_type=EsphomeBinarySensor, + state_type=BinarySensorState, +) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 6abd2eb9a00..1b0e4fc8986 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -49,6 +49,7 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, entity_registry as er, + issue_registry as ir, template, ) from homeassistant.helpers.device_registry import format_mac @@ -654,6 +655,30 @@ class ESPHomeManager: ): self._async_subscribe_logs(new_log_level) + @callback + def _async_cleanup(self) -> None: + """Cleanup stale issues and entities.""" + assert self.entry_data.device_info is not None + ent_reg = er.async_get(self.hass) + # Cleanup stale assist_in_progress entity and issue, + # Remove this after 2026.4 + if not ( + stale_entry_entity_id := ent_reg.async_get_entity_id( + DOMAIN, + Platform.BINARY_SENSOR, + f"{self.entry_data.device_info.mac_address}-assist_in_progress", + ) + ): + return + stale_entry = ent_reg.async_get(stale_entry_entity_id) + assert stale_entry is not None + ent_reg.async_remove(stale_entry_entity_id) + issue_reg = ir.async_get(self.hass) + if issue := issue_reg.async_get_issue( + DOMAIN, f"assist_in_progress_deprecated_{stale_entry.id}" + ): + issue_reg.async_delete(DOMAIN, issue.issue_id) + async def async_start(self) -> None: """Start the esphome connection manager.""" hass = self.hass @@ -696,6 +721,7 @@ class ESPHomeManager: _setup_services(hass, entry_data, services) if (device_info := entry_data.device_info) is not None: + self._async_cleanup() if device_info.name: reconnect_logic.name = device_info.name if ( diff --git a/homeassistant/components/esphome/repairs.py b/homeassistant/components/esphome/repairs.py index 42396fb8670..3cba8730cd6 100644 --- a/homeassistant/components/esphome/repairs.py +++ b/homeassistant/components/esphome/repairs.py @@ -7,9 +7,6 @@ from typing import cast import voluptuous as vol from homeassistant import data_entry_flow -from homeassistant.components.assist_pipeline.repair_flows import ( - AssistInProgressDeprecatedRepairFlow, -) from homeassistant.components.repairs import RepairsFlow from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import issue_registry as ir @@ -99,8 +96,6 @@ async def async_create_fix_flow( data: dict[str, str | int | float | None] | None, ) -> RepairsFlow: """Create flow.""" - if issue_id.startswith("assist_in_progress_deprecated"): - return AssistInProgressDeprecatedRepairFlow(data) if issue_id.startswith("device_conflict"): return DeviceConflictRepair(data) # If ESPHome adds confirm-only repairs in the future, this should be changed diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index fa4cc549250..f96a939588a 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -102,11 +102,6 @@ "name": "[%key:component::assist_satellite::entity_component::_::name%]" } }, - "binary_sensor": { - "assist_in_progress": { - "name": "[%key:component::assist_pipeline::entity::binary_sensor::assist_in_progress::name%]" - } - }, "select": { "pipeline": { "name": "[%key:component::assist_pipeline::entity::select::pipeline::name%]", diff --git a/tests/components/esphome/test_binary_sensor.py b/tests/components/esphome/test_binary_sensor.py index 25d8b60f574..9965c26f2e3 100644 --- a/tests/components/esphome/test_binary_sensor.py +++ b/tests/components/esphome/test_binary_sensor.py @@ -1,7 +1,6 @@ """Test ESPHome binary sensors.""" from collections.abc import Awaitable, Callable -from http import HTTPStatus from aioesphomeapi import ( APIClient, @@ -13,166 +12,12 @@ from aioesphomeapi import ( ) import pytest -from homeassistant.components.esphome import DOMAIN, DomainData -from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er, issue_registry as ir -from homeassistant.setup import async_setup_component from .conftest import MockESPHomeDevice from tests.common import MockConfigEntry -from tests.typing import ClientSessionGenerator - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_assist_in_progress( - hass: HomeAssistant, - mock_voice_assistant_v1_entry, -) -> None: - """Test assist in progress binary sensor.""" - - entry_data = DomainData.get(hass).get_entry_data(mock_voice_assistant_v1_entry) - - state = hass.states.get("binary_sensor.test_assist_in_progress") - assert state is not None - assert state.state == "off" - - entry_data.async_set_assist_pipeline_state(True) - - state = hass.states.get("binary_sensor.test_assist_in_progress") - assert state.state == "on" - - entry_data.async_set_assist_pipeline_state(False) - - state = hass.states.get("binary_sensor.test_assist_in_progress") - assert state.state == "off" - - -async def test_assist_in_progress_disabled_by_default( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - issue_registry: ir.IssueRegistry, - mock_voice_assistant_v1_entry, -) -> None: - """Test assist in progress binary sensor is added disabled.""" - - assert not hass.states.get("binary_sensor.test_assist_in_progress") - entity_entry = entity_registry.async_get("binary_sensor.test_assist_in_progress") - assert entity_entry - assert entity_entry.disabled - assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - - # Test no issue for disabled entity - assert len(issue_registry.issues) == 0 - - -@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, - mock_voice_assistant_v1_entry, -) -> None: - """Test assist in progress binary sensor.""" - - state = hass.states.get("binary_sensor.test_assist_in_progress") - assert state is not None - - entity_entry = entity_registry.async_get("binary_sensor.test_assist_in_progress") - 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( - "binary_sensor.test_assist_in_progress", - 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, - mock_voice_assistant_v1_entry, -) -> None: - """Test assist in progress binary sensor deprecation issue flow.""" - - state = hass.states.get("binary_sensor.test_assist_in_progress") - assert state is not None - - entity_entry = entity_registry.async_get("binary_sensor.test_assist_in_progress") - 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": "binary_sensor.test_assist_in_progress", - "entity_uuid": entity_entry.id, - "integration_name": "ESPHome", - } - assert issue.translation_key == "assist_in_progress_deprecated" - assert issue.translation_placeholders == {"integration_name": "ESPHome"} - - 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": "binary_sensor.test_assist_in_progress", - "integration_name": "ESPHome", - }, - "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("binary_sensor.test_assist_in_progress") - assert entity_entry.disabled_by is er.RegistryEntryDisabler.USER @pytest.mark.parametrize( diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index aa4ca665602..172b863229d 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -43,7 +43,11 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry as dr, issue_registry as ir +from homeassistant.helpers import ( + device_registry as dr, + entity_registry as er, + issue_registry as ir, +) from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.setup import async_setup_component @@ -1621,3 +1625,56 @@ async def test_device_adds_friendly_name( assert ( "No `friendly_name` set in the `esphome:` section of the YAML config for device" ) not in caplog.text + + +async def test_assist_in_progress_issue_deleted( + hass: HomeAssistant, + mock_client: APIClient, + entity_registry: er.EntityRegistry, + issue_registry: ir.IssueRegistry, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test assist in progress entity and issue is deleted. + + Remove this cleanup after 2026.4 + """ + entry = entity_registry.async_get_or_create( + domain=DOMAIN, + platform="binary_sensor", + unique_id="11:22:33:44:55:AA-assist_in_progress", + ) + ir.async_create_issue( + hass, + DOMAIN, + f"assist_in_progress_deprecated_{entry.id}", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="assist_in_progress_deprecated", + translation_placeholders={ + "integration_name": "ESPHome", + }, + ) + await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + device_info={}, + states=[], + mock_storage=True, + ) + assert ( + entity_registry.async_get_entity_id( + DOMAIN, "binary_sensor", "11:22:33:44:55:AA-assist_in_progress" + ) + is None + ) + assert ( + issue_registry.async_get_issue( + DOMAIN, f"assist_in_progress_deprecated_{entry.id}" + ) + is None + )