diff --git a/homeassistant/components/homekit_controller/entity.py b/homeassistant/components/homekit_controller/entity.py index 7e6838413b2383..92513a5c082cb9 100644 --- a/homeassistant/components/homekit_controller/entity.py +++ b/homeassistant/components/homekit_controller/entity.py @@ -286,26 +286,23 @@ def __init__( self._entity_key = (self._aid, self._iid, char.iid) @callback - def _async_remove_entity_if_characteristics_disappeared(self) -> bool: - """Handle characteristic disappearance.""" - if ( - not self._accessory.entity_map.aid(self._aid) + @override + def _async_config_changed(self) -> None: + """Handle accessory discovery changes.""" + if self._async_remove_entity_if_accessory_or_service_disappeared(): + return + + char = ( + self._accessory.entity_map.aid(self._aid) .services.iid(self._iid) .get_char_by_iid(self._char.iid) - ): + ) + if char is None: self._async_handle_entity_removed() - return True - return False + return - @callback - @override - def _async_config_changed(self) -> None: - """Handle accessory discovery changes.""" - if ( - not self._async_remove_entity_if_accessory_or_service_disappeared() - and not self._async_remove_entity_if_characteristics_disappeared() - ): - super()._async_reconfigure() + self._char = char + super()._async_reconfigure() class CharacteristicEntity(BaseCharacteristicEntity): diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index e79d3ab3edb49d..f3c08f0db9981a 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -7,6 +7,8 @@ from unittest import mock from aiohomekit import AccessoryNotFoundError +from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes from aiohomekit.testing import FakePairing from homeassistant.components.climate import ClimateEntityFeature @@ -140,6 +142,37 @@ async def test_ecobee3_setup(hass: HomeAssistant) -> None: ) +async def test_ecobee3_current_temperature_after_config_change( + hass: HomeAssistant, +) -> None: + """Test current temperature updates after the accessory map is replaced.""" + accessories = await setup_accessories_from_file(hass, "ecobee3.json") + await setup_test_accessories(hass, accessories) + + state = hass.states.get("sensor.homew_current_temperature") + assert state + assert state.state == "21.8" + + accessories = await setup_accessories_from_file(hass, "ecobee3.json") + thermostat = next( + accessory.services.first(service_type=ServicesTypes.THERMOSTAT) + for accessory in accessories + if accessory.aid == 1 + ) + assert thermostat + current_temperature = thermostat.characteristics.first( + char_types=[CharacteristicsTypes.TEMPERATURE_CURRENT] + ) + assert current_temperature + current_temperature.value = 22.4 + + await device_config_changed(hass, accessories) + + state = hass.states.get("sensor.homew_current_temperature") + assert state + assert state.state == "22.4" + + async def test_ecobee3_setup_from_cache( hass: HomeAssistant, entity_registry: er.EntityRegistry, diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index de51e43cf2f552..8a358797f269e4 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -2,7 +2,7 @@ from collections.abc import Callable -from aiohomekit.model import Accessory +from aiohomekit.model import Accessories, AccessoriesState, Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.characteristics.const import ThreadNodeCapabilities, ThreadStatus from aiohomekit.model.services import Service, ServicesTypes @@ -10,12 +10,14 @@ import pytest from homeassistant.components.homekit_controller.sensor import ( + SimpleSensor, thread_node_capability_to_str, thread_status_to_str, ) from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_component import DATA_INSTANCES from .common import TEST_DEVICE_SERVICE_INFO, Helper, setup_test_component @@ -83,6 +85,20 @@ def create_humidifier_with_water_level_sensor(accessory: Accessory) -> Service: return service +def create_accessory_without_humidifier(aid: int) -> Accessory: + """Create an accessory without the humidifier service.""" + return Accessory.create_with_info( + aid, "TestDevice", "example.com", "Test", "0001", "0.1" + ) + + +def create_humidifier_without_water_level(aid: int) -> Accessory: + """Create a humidifier accessory without the water level characteristic.""" + accessory = create_accessory_without_humidifier(aid) + accessory.add_service(ServicesTypes.HUMIDIFIER_DEHUMIDIFIER) + return accessory + + async def test_temperature_sensor_read_state( hass: HomeAssistant, get_next_aid: Callable[[], int] ) -> None: @@ -328,6 +344,48 @@ async def test_water_level_sensor_read_state( assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT +@pytest.mark.parametrize( + "replacement_accessory_factory", + [ + pytest.param( + create_accessory_without_humidifier, + id="service-removed", + ), + pytest.param( + create_humidifier_without_water_level, + id="characteristic-removed", + ), + ], +) +async def test_characteristic_sensor_removed_after_config_change( + hass: HomeAssistant, + get_next_aid: Callable[[], int], + replacement_accessory_factory: Callable[[int], Accessory], +) -> None: + """Test a characteristic sensor is removed when its source disappears.""" + aid = get_next_aid() + helper = await setup_test_component( + hass, aid, create_humidifier_with_water_level_sensor + ) + + assert hass.states.get("sensor.testdevice_water_level") is not None + entity = hass.data[DATA_INSTANCES]["sensor"].get_entity( + "sensor.testdevice_water_level" + ) + assert isinstance(entity, SimpleSensor) + + accessory = replacement_accessory_factory(aid) + accessories = Accessories() + accessories.add_accessory(accessory) + helper.pairing._accessories_state = AccessoriesState( + accessories, helper.pairing.config_num + 1 + ) + entity._async_config_changed() + await hass.async_block_till_done() + + assert hass.states.get("sensor.testdevice_water_level") is None + + def create_switch_with_sensor(accessory: Accessory) -> Service: """Define battery level characteristics.""" service = accessory.add_service(ServicesTypes.OUTLET)