diff --git a/homeassistant/components/acmeda/base.py b/homeassistant/components/acmeda/base.py index b325e2c944a..15f9716db47 100644 --- a/homeassistant/components/acmeda/base.py +++ b/homeassistant/components/acmeda/base.py @@ -32,7 +32,7 @@ class AcmedaBase(entity.Entity): device.id, remove_config_entry_id=self.registry_entry.config_entry_id ) - await self.async_remove() + await self.async_remove(force_remove=True) async def async_added_to_hass(self): """Entity has been added to hass.""" diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index ad5e4000116..d23c90bcb93 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -108,8 +108,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, Counter.from_yaml + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, Counter.from_yaml ) storage_collection = CounterStorageCollection( @@ -117,8 +117,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection( - component, storage_collection, Counter + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, Counter ) await yaml_collection.async_load( @@ -130,9 +130,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment") component.async_register_entity_service(SERVICE_DECREMENT, {}, "async_decrement") component.async_register_entity_service(SERVICE_RESET, {}, "async_reset") diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index c0c3d02ec56..0b3a7522845 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -1,5 +1,6 @@ """Support for esphome devices.""" import asyncio +import functools import logging import math from typing import Any, Callable, Dict, List, Optional @@ -520,7 +521,7 @@ class EsphomeBaseEntity(Entity): f"esphome_{self._entry_id}_remove_" f"{self._component_key}_{self._key}" ), - self.async_remove, + functools.partial(self.async_remove, force_remove=True), ) ) diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py index c45d6e56425..890c9f8e050 100644 --- a/homeassistant/components/gdacs/geo_location.py +++ b/homeassistant/components/gdacs/geo_location.py @@ -116,7 +116,7 @@ class GdacsEvent(GeolocationEvent): @callback def _delete_callback(self): """Remove this entity.""" - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @callback def _update_callback(self): diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index bb2d86539e9..40386648138 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -144,7 +144,7 @@ class GeoJsonLocationEvent(GeolocationEvent): """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @callback def _update_callback(self): diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index ed0b9f9f714..718b4c06b9c 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -102,7 +102,7 @@ class GeonetnzQuakesEvent(GeolocationEvent): @callback def _delete_callback(self): """Remove this entity.""" - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @callback def _update_callback(self): diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index a8df0107eeb..65e5ade7d1d 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -172,7 +172,7 @@ class HomematicipGenericEntity(Entity): """Handle hmip device removal.""" # Set marker showing that the HmIP device hase been removed. self.hmip_device_removed = True - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @property def name(self) -> str: diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py index 1760c59a69d..739e27d3360 100644 --- a/homeassistant/components/hue/helpers.py +++ b/homeassistant/components/hue/helpers.py @@ -17,7 +17,7 @@ async def remove_devices(bridge, api_ids, current): # Device is removed from Hue, so we remove it from Home Assistant entity = current[item_id] removed_items.append(item_id) - await entity.async_remove() + await entity.async_remove(force_remove=True) ent_registry = await get_ent_reg(bridge.hass) if entity.entity_id in ent_registry.entities: ent_registry.async_remove(entity.entity_id) diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index ce672194b9a..7bb8a75dfc7 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -1,6 +1,7 @@ """Support for Hyperion-NG remotes.""" from __future__ import annotations +import functools import logging from types import MappingProxyType from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple @@ -401,7 +402,7 @@ class HyperionBaseLight(LightEntity): async_dispatcher_connect( self.hass, SIGNAL_ENTITY_REMOVE.format(self._unique_id), - self.async_remove, + functools.partial(self.async_remove, force_remove=True), ) ) diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index 372e9876c35..9d90e1e12ef 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -1,5 +1,6 @@ """Switch platform for Hyperion.""" +import functools from typing import Any, Callable, Dict, Optional from hyperion import client @@ -199,7 +200,7 @@ class HyperionComponentSwitch(SwitchEntity): async_dispatcher_connect( self.hass, SIGNAL_ENTITY_REMOVE.format(self._unique_id), - self.async_remove, + functools.partial(self.async_remove, force_remove=True), ) ) diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index cc06110c111..0db580701d0 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -165,7 +165,7 @@ class IgnSismologiaLocationEvent(GeolocationEvent): """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @callback def _update_callback(self): diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index f123d6d3297..1b996722c01 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -89,8 +89,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, lambda conf: InputBoolean(conf, from_yaml=True) + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, InputBoolean.from_yaml ) storage_collection = InputBooleanStorageCollection( @@ -98,8 +98,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection( - component, storage_collection, InputBoolean + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, InputBoolean ) await yaml_collection.async_load( @@ -111,9 +111,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - async def reload_service_handler(service_call: ServiceCallType) -> None: """Remove all input booleans and load new ones from config.""" conf = await component.async_prepare_reload(skip_reset=True) @@ -146,14 +143,19 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: class InputBoolean(ToggleEntity, RestoreEntity): """Representation of a boolean input.""" - def __init__(self, config: typing.Optional[dict], from_yaml: bool = False): + def __init__(self, config: typing.Optional[dict]): """Initialize a boolean input.""" self._config = config - self._editable = True + self.editable = True self._state = config.get(CONF_INITIAL) - if from_yaml: - self._editable = False - self.entity_id = f"{DOMAIN}.{self.unique_id}" + + @classmethod + def from_yaml(cls, config: typing.Dict) -> "InputBoolean": + """Return entity instance initialized from yaml storage.""" + input_bool = cls(config) + input_bool.entity_id = f"{DOMAIN}.{config[CONF_ID]}" + input_bool.editable = False + return input_bool @property def should_poll(self): @@ -168,7 +170,7 @@ class InputBoolean(ToggleEntity, RestoreEntity): @property def state_attributes(self): """Return the state attributes of the entity.""" - return {ATTR_EDITABLE: self._editable} + return {ATTR_EDITABLE: self.editable} @property def icon(self): diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 0eab810245d..9589fe9a7ea 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -108,8 +108,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, InputDatetime.from_yaml + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, InputDatetime.from_yaml ) storage_collection = DateTimeStorageCollection( @@ -117,8 +117,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection( - component, storage_collection, InputDatetime + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, InputDatetime ) await yaml_collection.async_load( @@ -130,9 +130,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - async def reload_service_handler(service_call: ServiceCallType) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 1f979cad7a9..5cad0f49c88 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -119,8 +119,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, InputNumber.from_yaml + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, InputNumber.from_yaml ) storage_collection = NumberStorageCollection( @@ -128,8 +128,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection( - component, storage_collection, InputNumber + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, InputNumber ) await yaml_collection.async_load( @@ -141,9 +141,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - async def reload_service_handler(service_call: ServiceCallType) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 6272992f243..a390d8e1901 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -94,8 +94,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, InputSelect.from_yaml + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, InputSelect.from_yaml ) storage_collection = InputSelectStorageCollection( @@ -103,8 +103,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection( - component, storage_collection, InputSelect + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, InputSelect ) await yaml_collection.async_load( @@ -116,9 +116,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - async def reload_service_handler(service_call: ServiceCallType) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index c512bc221db..76eb51eedd5 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -119,8 +119,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, InputText.from_yaml + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, InputText.from_yaml ) storage_collection = InputTextStorageCollection( @@ -128,8 +128,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection( - component, storage_collection, InputText + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, InputText ) await yaml_collection.async_load( @@ -141,9 +141,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - async def reload_service_handler(service_call: ServiceCallType) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index e2b9dd39f34..2234eb4750c 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -1,4 +1,5 @@ """Insteon base entity.""" +import functools import logging from pyinsteon import devices @@ -122,7 +123,11 @@ class InsteonEntity(Entity): ) remove_signal = f"{self._insteon_device.address.id}_{SIGNAL_REMOVE_ENTITY}" self.async_on_remove( - async_dispatcher_connect(self.hass, remove_signal, self.async_remove) + async_dispatcher_connect( + self.hass, + remove_signal, + functools.partial(self.async_remove, force_remove=True), + ) ) async def async_will_remove_from_hass(self): diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 1ab2054b355..8d9c9533ed3 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -387,7 +387,7 @@ class MqttDiscoveryUpdate(Entity): entity_registry.async_remove(self.entity_id) await cleanup_device_registry(self.hass, entity_entry.device_id) else: - await self.async_remove() + await self.async_remove(force_remove=True) async def discovery_callback(payload): """Handle discovery update.""" diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py index c8eda3690ef..12ae9d8990a 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -210,7 +210,7 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): @callback def _delete_callback(self): """Remove this entity.""" - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @callback def _update_callback(self): diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 3a4aac6bfd1..bd01284329b 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -304,7 +304,7 @@ async def async_handle_waypoint(hass, name_base, waypoint): if hass.states.get(entity_id) is not None: return - zone = zone_comp.Zone( + zone = zone_comp.Zone.from_yaml( { zone_comp.CONF_NAME: pretty_name, zone_comp.CONF_LATITUDE: lat, @@ -313,7 +313,6 @@ async def async_handle_waypoint(hass, name_base, waypoint): zone_comp.CONF_ICON: zone_comp.ICON_IMPORT, zone_comp.CONF_PASSIVE: False, }, - False, ) zone.hass = hass zone.entity_id = entity_id diff --git a/homeassistant/components/ozw/entity.py b/homeassistant/components/ozw/entity.py index 9c494a514e0..c1cb9617a5c 100644 --- a/homeassistant/components/ozw/entity.py +++ b/homeassistant/components/ozw/entity.py @@ -268,7 +268,7 @@ class ZWaveDeviceEntity(Entity): if not self.values: return # race condition: delete already requested if values_id == self.values.values_id: - await self.async_remove() + await self.async_remove(force_remove=True) def create_device_name(node: OZWNode): diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index d0c0e9eccc8..d3e17d904ea 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -306,14 +306,12 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): yaml_collection, ) - collection.attach_entity_component_collection( - entity_component, yaml_collection, lambda conf: Person(conf, False) + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, entity_component, yaml_collection, Person ) - collection.attach_entity_component_collection( - entity_component, storage_collection, lambda conf: Person(conf, True) + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, entity_component, storage_collection, Person.from_yaml ) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) await yaml_collection.async_load( await filter_yaml_data(hass, config.get(DOMAIN, [])) @@ -358,10 +356,10 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): class Person(RestoreEntity): """Represent a tracked person.""" - def __init__(self, config, editable): + def __init__(self, config): """Set up person.""" self._config = config - self._editable = editable + self.editable = True self._latitude = None self._longitude = None self._gps_accuracy = None @@ -369,6 +367,13 @@ class Person(RestoreEntity): self._state = None self._unsub_track_device = None + @classmethod + def from_yaml(cls, config): + """Return entity instance initialized from yaml storage.""" + person = cls(config) + person.editable = False + return person + @property def name(self): """Return the name of the entity.""" @@ -395,7 +400,7 @@ class Person(RestoreEntity): @property def state_attributes(self): """Return the state attributes of the person.""" - data = {ATTR_EDITABLE: self._editable, ATTR_ID: self.unique_id} + data = {ATTR_EDITABLE: self.editable, ATTR_ID: self.unique_id} if self._latitude is not None: data[ATTR_LATITUDE] = self._latitude if self._longitude is not None: diff --git a/homeassistant/components/qld_bushfire/geo_location.py b/homeassistant/components/qld_bushfire/geo_location.py index 8efb1a32705..f608f6e12ae 100644 --- a/homeassistant/components/qld_bushfire/geo_location.py +++ b/homeassistant/components/qld_bushfire/geo_location.py @@ -167,7 +167,7 @@ class QldBushfireLocationEvent(GeolocationEvent): """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @callback def _update_callback(self): diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 067ffeb5313..5952cb62a71 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -3,6 +3,7 @@ import asyncio import binascii from collections import OrderedDict import copy +import functools import logging import RFXtrx as rfxtrxmod @@ -488,7 +489,8 @@ class RfxtrxEntity(RestoreEntity): self.async_on_remove( self.hass.helpers.dispatcher.async_dispatcher_connect( - f"{DOMAIN}_{CONF_REMOVE_DEVICE}_{self._device_id}", self.async_remove + f"{DOMAIN}_{CONF_REMOVE_DEVICE}_{self._device_id}", + functools.partial(self.async_remove, force_remove=True), ) ) diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 94efe9b98c7..fa94ca4e384 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -244,7 +244,7 @@ class SeventeenTrackPackageSensor(Entity): async def _remove(self, *_): """Remove entity itself.""" - await self.async_remove() + await self.async_remove(force_remove=True) reg = await self.hass.helpers.entity_registry.async_get_registry() entity_id = reg.async_get_entity_id( diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 64d651b4cd8..b123bbadf7d 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -107,8 +107,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, Timer.from_yaml + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, Timer.from_yaml ) storage_collection = TimerStorageCollection( @@ -116,7 +116,9 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection(component, storage_collection, Timer) + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, Timer + ) await yaml_collection.async_load( [{CONF_ID: id_, **cfg} for id_, cfg in config.get(DOMAIN, {}).items()] @@ -127,9 +129,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - async def reload_service_handler(service_call: ServiceCallType) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 5876331ea97..7f6ba6b26fd 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -392,7 +392,7 @@ class TuyaDevice(Entity): entity_registry.async_remove(self.entity_id) await cleanup_device_registry(self.hass, entity_entry.device_id) else: - await self.async_remove() + await self.async_remove(force_remove=True) @callback def _update_callback(self): diff --git a/homeassistant/components/unifi/unifi_entity_base.py b/homeassistant/components/unifi/unifi_entity_base.py index 904348f6324..03c63ce4e84 100644 --- a/homeassistant/components/unifi/unifi_entity_base.py +++ b/homeassistant/components/unifi/unifi_entity_base.py @@ -91,7 +91,7 @@ class UniFiBase(Entity): entity_registry = await self.hass.helpers.entity_registry.async_get_registry() entity_entry = entity_registry.async_get(self.entity_id) if not entity_entry: - await self.async_remove() + await self.async_remove(force_remove=True) return device_registry = await self.hass.helpers.device_registry.async_get_registry() diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index 40a544a2e21..2b149fcac26 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -210,7 +210,7 @@ class UsgsEarthquakesEvent(GeolocationEvent): """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @callback def _update_callback(self): diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 527d985a47b..f89cf06a44c 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -442,7 +442,7 @@ async def async_remove_entity( ) -> None: """Remove WLED segment light from Home Assistant.""" entity = current[index] - await entity.async_remove() + await entity.async_remove(force_remove=True) registry = await async_get_entity_registry(coordinator.hass) if entity.entity_id in registry.entities: registry.async_remove(entity.entity_id) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 96f005ba288..db30e9e178c 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -1,6 +1,7 @@ """Entity for Zigbee Home Automation.""" import asyncio +import functools import logging from typing import Any, Awaitable, Dict, List, Optional @@ -165,7 +166,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): self.async_accept_signal( None, f"{SIGNAL_REMOVE}_{self.zha_device.ieee}", - self.async_remove, + functools.partial(self.async_remove, force_remove=True), signal_override=True, ) @@ -239,7 +240,7 @@ class ZhaGroupEntity(BaseZhaEntity): return self._handled_group_membership = True - await self.async_remove() + await self.async_remove(force_remove=True) async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 01a8b9aa0f4..1eef9636e36 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -25,7 +25,6 @@ from homeassistant.helpers import ( config_validation as cv, entity, entity_component, - entity_registry, service, storage, ) @@ -183,8 +182,8 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: yaml_collection = collection.IDLessCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, lambda conf: Zone(conf, False) + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, Zone.from_yaml ) storage_collection = ZoneStorageCollection( @@ -192,8 +191,8 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection( - component, storage_collection, lambda conf: Zone(conf, True) + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, Zone ) if config[DOMAIN]: @@ -205,18 +204,6 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - async def _collection_changed(change_type: str, item_id: str, config: Dict) -> None: - """Handle a collection change: clean up entity registry on removals.""" - if change_type != collection.CHANGE_REMOVED: - return - - ent_reg = await entity_registry.async_get_registry(hass) - ent_reg.async_remove( - cast(str, ent_reg.async_get_entity_id(DOMAIN, DOMAIN, item_id)) - ) - - storage_collection.async_add_listener(_collection_changed) - async def reload_service_handler(service_call: ServiceCall) -> None: """Remove all zones and load new ones from config.""" conf = await component.async_prepare_reload(skip_reset=True) @@ -235,10 +222,7 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: if component.get_entity("zone.home"): return True - home_zone = Zone( - _home_conf(hass), - True, - ) + home_zone = Zone(_home_conf(hass)) home_zone.entity_id = ENTITY_ID_HOME await component.async_add_entities([home_zone]) @@ -293,13 +277,21 @@ async def async_unload_entry( class Zone(entity.Entity): """Representation of a Zone.""" - def __init__(self, config: Dict, editable: bool): + def __init__(self, config: Dict): """Initialize the zone.""" self._config = config - self._editable = editable + self.editable = True self._attrs: Optional[Dict] = None self._generate_attrs() + @classmethod + def from_yaml(cls, config: Dict) -> "Zone": + """Return entity instance initialized from yaml storage.""" + zone = cls(config) + zone.editable = False + zone._generate_attrs() # pylint:disable=protected-access + return zone + @property def state(self) -> str: """Return the state property really does nothing for a zone.""" @@ -346,5 +338,5 @@ class Zone(entity.Entity): ATTR_LONGITUDE: self._config[CONF_LONGITUDE], ATTR_RADIUS: self._config[CONF_RADIUS], ATTR_PASSIVE: self._config[CONF_PASSIVE], - ATTR_EDITABLE: self._editable, + ATTR_EDITABLE: self.editable, } diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 56dea1639a3..faaea30e0ee 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -95,7 +95,7 @@ class ZWaveBaseEntity(Entity): """Remove this entity and add it back.""" async def _async_remove_and_add(): - await self.async_remove() + await self.async_remove(force_remove=True) self.entity_id = None await self.platform.async_add_entities([self]) @@ -104,7 +104,7 @@ class ZWaveBaseEntity(Entity): async def node_removed(self): """Call when a node is removed from the Z-Wave network.""" - await self.async_remove() + await self.async_remove(force_remove=True) registry = await async_get_registry(self.hass) if self.entity_id not in registry.entities: diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 6733b1d3dbd..4af524bbbc9 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -301,7 +301,10 @@ class IDLessCollection(ObservableCollection): @callback -def attach_entity_component_collection( +def sync_entity_lifecycle( + hass: HomeAssistantType, + domain: str, + platform: str, entity_component: EntityComponent, collection: ObservableCollection, create_entity: Callable[[dict], Entity], @@ -318,8 +321,13 @@ def attach_entity_component_collection( return if change_type == CHANGE_REMOVED: - entity = entities.pop(item_id) - await entity.async_remove() + ent_reg = await entity_registry.async_get_registry(hass) + ent_to_remove = ent_reg.async_get_entity_id(domain, platform, item_id) + if ent_to_remove is not None: + ent_reg.async_remove(ent_to_remove) + else: + await entities[item_id].async_remove(force_remove=True) + entities.pop(item_id) return # CHANGE_UPDATED @@ -328,28 +336,6 @@ def attach_entity_component_collection( collection.async_add_listener(_collection_changed) -@callback -def attach_entity_registry_cleaner( - hass: HomeAssistantType, - domain: str, - platform: str, - collection: ObservableCollection, -) -> None: - """Attach a listener to clean up entity registry on collection changes.""" - - async def _collection_changed(change_type: str, item_id: str, config: Dict) -> None: - """Handle a collection change: clean up entity registry on removals.""" - if change_type != CHANGE_REMOVED: - return - - ent_reg = await entity_registry.async_get_registry(hass) - ent_to_remove = ent_reg.async_get_entity_id(domain, platform, item_id) - if ent_to_remove is not None: - ent_reg.async_remove(ent_to_remove) - - collection.async_add_listener(_collection_changed) - - class StorageCollectionWebsocket: """Class to expose storage collection management over websocket.""" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 03342a9f235..04c07ef0f36 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -530,8 +530,16 @@ class Entity(ABC): await self.async_added_to_hass() self.async_write_ha_state() - async def async_remove(self) -> None: - """Remove entity from Home Assistant.""" + async def async_remove(self, *, force_remove: bool = False) -> None: + """Remove entity from Home Assistant. + + If the entity has a non disabled entry in the entity registry, + the entity's state will be set to unavailable, in the same way + as when the entity registry is loaded. + + If the entity doesn't have a non disabled entry in the entity registry, + or if force_remove=True, its state will be removed. + """ assert self.hass is not None if self.platform and not self._added: @@ -548,7 +556,16 @@ class Entity(ABC): await self.async_internal_will_remove_from_hass() await self.async_will_remove_from_hass() - self.hass.states.async_remove(self.entity_id, context=self._context) + # Check if entry still exists in entity registry (e.g. unloading config entry) + if ( + not force_remove + and self.registry_entry + and not self.registry_entry.disabled + ): + # Set the entity's state will to unavailable + ATTR_RESTORED: True + self.registry_entry.write_unavailable_state(self.hass) + else: + self.hass.states.async_remove(self.entity_id, context=self._context) async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass. @@ -606,6 +623,7 @@ class Entity(ABC): data = event.data if data["action"] == "remove": await self.async_removed_from_registry() + self.registry_entry = None await self.async_remove() if data["action"] != "update": @@ -617,7 +635,7 @@ class Entity(ABC): self.registry_entry = ent_reg.async_get(data["entity_id"]) assert self.registry_entry is not None - if self.registry_entry.disabled_by is not None: + if self.registry_entry.disabled: await self.async_remove() return @@ -626,7 +644,7 @@ class Entity(ABC): self.async_write_ha_state() return - await self.async_remove() + await self.async_remove(force_remove=True) assert self.platform is not None self.entity_id = self.registry_entry.entity_id diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index bd687ab7ce8..26fec28c047 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -517,7 +517,7 @@ class EntityPlatform: if not self.entities: return - tasks = [self.async_remove_entity(entity_id) for entity_id in self.entities] + tasks = [entity.async_remove() for entity in self.entities.values()] await asyncio.gather(*tasks) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 0628c1e0eb5..15218afc227 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -115,6 +115,33 @@ class RegistryEntry: """Return if entry is disabled.""" return self.disabled_by is not None + @callback + def write_unavailable_state(self, hass: HomeAssistantType) -> None: + """Write the unavailable state to the state machine.""" + attrs: Dict[str, Any] = {ATTR_RESTORED: True} + + if self.capabilities is not None: + attrs.update(self.capabilities) + + if self.supported_features is not None: + attrs[ATTR_SUPPORTED_FEATURES] = self.supported_features + + if self.device_class is not None: + attrs[ATTR_DEVICE_CLASS] = self.device_class + + if self.unit_of_measurement is not None: + attrs[ATTR_UNIT_OF_MEASUREMENT] = self.unit_of_measurement + + name = self.name or self.original_name + if name is not None: + attrs[ATTR_FRIENDLY_NAME] = name + + icon = self.icon or self.original_icon + if icon is not None: + attrs[ATTR_ICON] = icon + + hass.states.async_set(self.entity_id, STATE_UNAVAILABLE, attrs) + class EntityRegistry: """Class to hold a registry of entities.""" @@ -616,36 +643,13 @@ def async_setup_entity_restore( @callback def _write_unavailable_states(_: Event) -> None: """Make sure state machine contains entry for each registered entity.""" - states = hass.states - existing = set(states.async_entity_ids()) + existing = set(hass.states.async_entity_ids()) for entry in registry.entities.values(): if entry.entity_id in existing or entry.disabled: continue - attrs: Dict[str, Any] = {ATTR_RESTORED: True} - - if entry.capabilities is not None: - attrs.update(entry.capabilities) - - if entry.supported_features is not None: - attrs[ATTR_SUPPORTED_FEATURES] = entry.supported_features - - if entry.device_class is not None: - attrs[ATTR_DEVICE_CLASS] = entry.device_class - - if entry.unit_of_measurement is not None: - attrs[ATTR_UNIT_OF_MEASUREMENT] = entry.unit_of_measurement - - name = entry.name or entry.original_name - if name is not None: - attrs[ATTR_FRIENDLY_NAME] = name - - icon = entry.icon or entry.original_icon - if icon is not None: - attrs[ATTR_ICON] = icon - - states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs) + entry.write_unavailable_state(hass) hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states) diff --git a/tests/components/cert_expiry/test_init.py b/tests/components/cert_expiry/test_init.py index ea31ba50ea0..1c62782107b 100644 --- a/tests/components/cert_expiry/test_init.py +++ b/tests/components/cert_expiry/test_init.py @@ -5,7 +5,12 @@ from unittest.mock import patch from homeassistant.components.cert_expiry.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED -from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_START +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_START, + STATE_UNAVAILABLE, +) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -94,4 +99,9 @@ async def test_unload_config_entry(mock_now, hass): assert entry.state == ENTRY_STATE_NOT_LOADED state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state is None diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 3611e30f665..f64a4c4c259 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -16,7 +16,7 @@ from homeassistant.components.deconz.const import ( ) from homeassistant.components.deconz.gateway import get_gateway_from_config_entry from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.setup import async_setup_component @@ -111,6 +111,10 @@ async def test_binary_sensors(hass): await hass.config_entries.async_unload(config_entry.entry_id) + assert hass.states.get("binary_sensor.presence_sensor").state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 4d68ba2a6a7..fcb6e16f07f 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -39,7 +39,12 @@ from homeassistant.components.deconz.const import ( DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.deconz.gateway import get_gateway_from_config_entry -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_OFF +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + STATE_OFF, + STATE_UNAVAILABLE, +) from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -361,6 +366,13 @@ async def test_climate_device_without_cooling_support(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 2 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 5314a41b315..43364208f4f 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -19,7 +19,12 @@ from homeassistant.components.cover import ( ) from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.gateway import get_gateway_from_config_entry -from homeassistant.const import ATTR_ENTITY_ID, STATE_CLOSED, STATE_OPEN +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_CLOSED, + STATE_OPEN, + STATE_UNAVAILABLE, +) from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -251,6 +256,13 @@ async def test_cover(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 5 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 14faf1a938c..232de5eacd2 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -4,6 +4,7 @@ from copy import deepcopy from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT from homeassistant.components.deconz.gateway import get_gateway_from_config_entry +from homeassistant.const import STATE_UNAVAILABLE from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -121,5 +122,13 @@ async def test_deconz_events(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 3 + for state in states: + assert state.state == STATE_UNAVAILABLE + assert len(gateway.events) == 0 + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 assert len(gateway.events) == 0 diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index b9c154a2791..7f225196744 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -18,7 +18,7 @@ from homeassistant.components.fan import ( SPEED_MEDIUM, SPEED_OFF, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -207,4 +207,11 @@ async def test_fans(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 2 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 20fb50247ee..bdb7fbb8aef 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -31,6 +31,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.setup import async_setup_component @@ -296,6 +297,13 @@ async def test_lights_and_groups(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 6 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py index 7e9b8233778..d53da74dfdd 100644 --- a/tests/components/deconz/test_lock.py +++ b/tests/components/deconz/test_lock.py @@ -10,7 +10,12 @@ from homeassistant.components.lock import ( SERVICE_LOCK, SERVICE_UNLOCK, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_LOCKED, + STATE_UNAVAILABLE, + STATE_UNLOCKED, +) from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -104,4 +109,11 @@ async def test_locks(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 1 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index def2a1412e5..426a88b8bb6 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, + STATE_UNAVAILABLE, ) from homeassistant.setup import async_setup_component @@ -165,6 +166,13 @@ async def test_sensors(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 5 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index e42e89d903e..22ce182cb62 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -10,7 +10,7 @@ from homeassistant.components.switch import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -139,6 +139,13 @@ async def test_power_plugs(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 4 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 @@ -202,4 +209,11 @@ async def test_sirens(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 2 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/dynalite/test_light.py b/tests/components/dynalite/test_light.py index 7df10fb08e8..230e7584d70 100644 --- a/tests/components/dynalite/test_light.py +++ b/tests/components/dynalite/test_light.py @@ -4,7 +4,11 @@ from dynalite_devices_lib.light import DynaliteChannelLightDevice import pytest from homeassistant.components.light import SUPPORT_BRIGHTNESS -from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, + STATE_UNAVAILABLE, +) from .common import ( ATTR_METHOD, @@ -40,11 +44,21 @@ async def test_light_setup(hass, mock_device): ) -async def test_remove_entity(hass, mock_device): - """Test when an entity is removed from HA.""" +async def test_unload_config_entry(hass, mock_device): + """Test when a config entry is unloaded from HA.""" await create_entity_from_device(hass, mock_device) assert hass.states.get("light.name") entry_id = await get_entry_id_from_hass(hass) assert await hass.config_entries.async_unload(entry_id) await hass.async_block_till_done() + assert hass.states.get("light.name").state == STATE_UNAVAILABLE + + +async def test_remove_config_entry(hass, mock_device): + """Test when a config entry is removed from HA.""" + await create_entity_from_device(hass, mock_device) + assert hass.states.get("light.name") + entry_id = await get_entry_id_from_hass(hass) + assert await hass.config_entries.async_remove(entry_id) + await hass.async_block_till_done() assert not hass.states.get("light.name") diff --git a/tests/components/eafm/test_sensor.py b/tests/components/eafm/test_sensor.py index a7ee0403c7c..3f2eb72a8e3 100644 --- a/tests/components/eafm/test_sensor.py +++ b/tests/components/eafm/test_sensor.py @@ -5,7 +5,7 @@ import aiohttp import pytest from homeassistant import config_entries -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -428,5 +428,8 @@ async def test_unload_entry(hass, mock_get_station): assert await entry.async_unload(hass) - # And the entity should be gone - assert not hass.states.get("sensor.my_station_water_level_stage") + # And the entity should be unavailable + assert ( + hass.states.get("sensor.my_station_water_level_stage").state + == STATE_UNAVAILABLE + ) diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 149cbdae4e2..ffbf7a569f9 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -345,12 +345,12 @@ async def mock_api_object_fixture(hass, config_entry, get_request_return_values) async def test_unload_config_entry(hass, config_entry, mock_api_object): - """Test the player is removed when the config entry is unloaded.""" + """Test the player is set unavailable when the config entry is unloaded.""" assert hass.states.get(TEST_MASTER_ENTITY_NAME) assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]) await config_entry.async_unload(hass) - assert not hass.states.get(TEST_MASTER_ENTITY_NAME) - assert not hass.states.get(TEST_ZONE_ENTITY_NAMES[0]) + assert hass.states.get(TEST_MASTER_ENTITY_NAME).state == STATE_UNAVAILABLE + assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]).state == STATE_UNAVAILABLE def test_master_state(hass, mock_api_object): diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 11067c1aa51..08655033f4d 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -4,7 +4,13 @@ from unittest.mock import Mock, call from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_DEVICES, + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + STATE_UNAVAILABLE, +) from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component @@ -45,8 +51,8 @@ async def test_setup_duplicate_config(hass: HomeAssistantType, fritz: Mock, capl assert "duplicate host entries found" in caplog.text -async def test_unload(hass: HomeAssistantType, fritz: Mock): - """Test unload of integration.""" +async def test_unload_remove(hass: HomeAssistantType, fritz: Mock): + """Test unload and remove of integration.""" fritz().get_devices.return_value = [FritzDeviceSwitchMock()] entity_id = f"{SWITCH_DOMAIN}.fake_name" @@ -70,6 +76,14 @@ async def test_unload(hass: HomeAssistantType, fritz: Mock): await hass.config_entries.async_unload(entry.entry_id) + assert fritz().logout.call_count == 1 + assert entry.state == ENTRY_STATE_NOT_LOADED + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + assert fritz().logout.call_count == 1 assert entry.state == ENTRY_STATE_NOT_LOADED state = hass.states.get(entity_id) diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index ef7285ab185..4d979f8e556 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -587,10 +587,10 @@ async def test_select_input_command_error( async def test_unload_config_entry(hass, config_entry, config, controller): - """Test the player is removed when the config entry is unloaded.""" + """Test the player is set unavailable when the config entry is unloaded.""" await setup_platform(hass, config_entry, config) await config_entry.async_unload(hass) - assert not hass.states.get("media_player.test_player") + assert hass.states.get("media_player.test_player").state == STATE_UNAVAILABLE async def test_play_media_url(hass, config_entry, config, controller, caplog): diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py index e443e36b910..f4950512063 100644 --- a/tests/components/homekit_controller/test_light.py +++ b/tests/components/homekit_controller/test_light.py @@ -3,6 +3,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from homeassistant.components.homekit_controller.const import KNOWN_DEVICES +from homeassistant.const import STATE_UNAVAILABLE from tests.components.homekit_controller.common import setup_test_component @@ -209,8 +210,8 @@ async def test_light_becomes_unavailable_but_recovers(hass, utcnow): assert state.attributes["color_temp"] == 400 -async def test_light_unloaded(hass, utcnow): - """Test entity and HKDevice are correctly unloaded.""" +async def test_light_unloaded_removed(hass, utcnow): + """Test entity and HKDevice are correctly unloaded and removed.""" helper = await setup_test_component(hass, create_lightbulb_service_with_color_temp) # Initial state is that the light is off @@ -220,9 +221,15 @@ async def test_light_unloaded(hass, utcnow): unload_result = await helper.config_entry.async_unload(hass) assert unload_result is True - # Make sure entity is unloaded - assert hass.states.get(helper.entity_id) is None + # Make sure entity is set to unavailable state + assert hass.states.get(helper.entity_id).state == STATE_UNAVAILABLE # Make sure HKDevice is no longer set to poll this accessory conn = hass.data[KNOWN_DEVICES]["00:00:00:00:00:00"] assert not conn.pollable_characteristics + + await helper.config_entry.async_remove(hass) + await hass.async_block_till_done() + + # Make sure entity is removed + assert hass.states.get(helper.entity_id).state == STATE_UNAVAILABLE diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py index 96be450f7e4..3de6af83e46 100644 --- a/tests/components/huisbaasje/test_init.py +++ b/tests/components/huisbaasje/test_init.py @@ -11,7 +11,7 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_ERROR, ConfigEntry, ) -from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -145,6 +145,14 @@ async def test_unload_entry(hass: HomeAssistant): await hass.config_entries.async_unload(config_entry.entry_id) assert config_entry.state == ENTRY_STATE_NOT_LOADED entities = hass.states.async_entity_ids("sensor") + assert len(entities) == 14 + for entity in entities: + assert hass.states.get(entity).state == STATE_UNAVAILABLE + + # Remove config entry + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + entities = hass.states.async_entity_ids("sensor") assert len(entities) == 0 # Assert mocks are called diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index 88562678436..c5d4d40e0d5 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -264,7 +264,7 @@ async def test_reload(hass, hass_admin_user): assert "mdi:work_reloaded" == state_2.attributes.get(ATTR_ICON) -async def test_load_person_storage(hass, storage_setup): +async def test_load_from_storage(hass, storage_setup): """Test set up from storage.""" assert await storage_setup() state = hass.states.get(f"{DOMAIN}.from_storage") diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index 242352c2498..24a81be3896 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -30,6 +30,7 @@ async def test_tracking_home(hass, mock_weather): entry = hass.config_entries.async_entries()[0] await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_entity_ids("weather")) == 0 @@ -63,4 +64,5 @@ async def test_not_tracking_home(hass, mock_weather): entry = hass.config_entries.async_entries()[0] await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_entity_ids("weather")) == 0 diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index 84deef92d62..117c8e97884 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -339,6 +339,7 @@ async def test_camera_removed(hass, auth): for config_entry in hass.config_entries.async_entries(DOMAIN): await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/nws/test_init.py b/tests/components/nws/test_init.py index 44b5193d79c..01a203aa07b 100644 --- a/tests/components/nws/test_init.py +++ b/tests/components/nws/test_init.py @@ -1,6 +1,7 @@ """Tests for init module.""" from homeassistant.components.nws.const import DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN +from homeassistant.const import STATE_UNAVAILABLE from tests.common import MockConfigEntry from tests.components.nws.const import NWS_CONFIG @@ -25,5 +26,12 @@ async def test_unload_entry(hass, mock_simple_nws): assert len(entries) == 1 assert await hass.config_entries.async_unload(entries[0].entry_id) - assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 + entities = hass.states.async_entity_ids(WEATHER_DOMAIN) + assert len(entities) == 1 + for entity in entities: + assert hass.states.get(entity).state == STATE_UNAVAILABLE assert DOMAIN not in hass.data + + assert await hass.config_entries.async_remove(entries[0].entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py index 2e57c4c01f3..339b690f4e4 100644 --- a/tests/components/ozw/test_init.py +++ b/tests/components/ozw/test_init.py @@ -4,6 +4,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.ozw import DOMAIN, PLATFORMS, const +from homeassistant.const import ATTR_RESTORED, STATE_UNAVAILABLE from .common import setup_ozw @@ -76,14 +77,21 @@ async def test_unload_entry(hass, generic_data, switch_msg, caplog): await hass.config_entries.async_unload(entry.entry_id) assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED - assert len(hass.states.async_entity_ids("switch")) == 0 + entities = hass.states.async_entity_ids("switch") + assert len(entities) == 1 + for entity in entities: + assert hass.states.get(entity).state == STATE_UNAVAILABLE + assert hass.states.get(entity).attributes.get(ATTR_RESTORED) # Send a message for a switch from the broker to check that # all entity topic subscribers are unsubscribed. receive_message(switch_msg) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("switch")) == 0 + assert len(hass.states.async_entity_ids("switch")) == 1 + for entity in entities: + assert hass.states.get(entity).state == STATE_UNAVAILABLE + assert hass.states.get(entity).attributes.get(ATTR_RESTORED) # Load the integration again and check that there are no errors when # adding the entities. diff --git a/tests/components/panasonic_viera/test_init.py b/tests/components/panasonic_viera/test_init.py index 8f95043f4fa..5c9bf183c6f 100644 --- a/tests/components/panasonic_viera/test_init.py +++ b/tests/components/panasonic_viera/test_init.py @@ -17,7 +17,7 @@ from homeassistant.components.panasonic_viera.const import ( DOMAIN, ) from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_UNAVAILABLE from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -253,9 +253,11 @@ async def test_setup_unload_entry(hass): await hass.async_block_till_done() await hass.config_entries.async_unload(mock_entry.entry_id) - assert mock_entry.state == ENTRY_STATE_NOT_LOADED - state = hass.states.get("media_player.panasonic_viera_tv") + assert state.state == STATE_UNAVAILABLE + await hass.config_entries.async_remove(mock_entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get("media_player.panasonic_viera_tv") assert state is None diff --git a/tests/components/plex/test_media_players.py b/tests/components/plex/test_media_players.py index 092d7e09008..fbd1205b2ef 100644 --- a/tests/components/plex/test_media_players.py +++ b/tests/components/plex/test_media_players.py @@ -22,7 +22,7 @@ async def test_plex_tv_clients( media_players_after = len(hass.states.async_entity_ids("media_player")) assert media_players_after == media_players_before + 1 - await hass.config_entries.async_unload(entry.entry_id) + await hass.config_entries.async_remove(entry.entry_id) # Ensure only plex.tv resource client is found with patch("plexapi.server.PlexServer.sessions", return_value=[]): diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 6931b3dfbb5..e10d63a2e07 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -12,7 +12,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.components.smartthings import binary_sensor from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE -from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform @@ -93,4 +93,7 @@ async def test_unload_config_entry(hass, device_factory): # Act await hass.config_entries.async_forward_entry_unload(config_entry, "binary_sensor") # Assert - assert not hass.states.get("binary_sensor.motion_sensor_1_motion") + assert ( + hass.states.get("binary_sensor.motion_sensor_1_motion").state + == STATE_UNAVAILABLE + ) diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 0483480cb8a..178c905208e 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -19,7 +19,7 @@ from homeassistant.components.cover import ( STATE_OPENING, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform @@ -193,4 +193,4 @@ async def test_unload_config_entry(hass, device_factory): # Act await hass.config_entries.async_forward_entry_unload(config_entry, COVER_DOMAIN) # Assert - assert not hass.states.get("cover.garage") + assert hass.states.get("cover.garage").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 0ebef7e7323..1f837d58bf8 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -17,7 +17,11 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE -from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + STATE_UNAVAILABLE, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform @@ -184,4 +188,4 @@ async def test_unload_config_entry(hass, device_factory): # Act await hass.config_entries.async_forward_entry_unload(config_entry, "fan") # Assert - assert not hass.states.get("fan.fan_1") + assert hass.states.get("fan.fan_1").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index bd9557c6b97..f6d7d8dd9f4 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -19,7 +19,11 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE -from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + STATE_UNAVAILABLE, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform @@ -304,4 +308,4 @@ async def test_unload_config_entry(hass, device_factory): # Act await hass.config_entries.async_forward_entry_unload(config_entry, "light") # Assert - assert not hass.states.get("light.color_dimmer_2") + assert hass.states.get("light.color_dimmer_2").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 0492f2281ce..185eae22ccf 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -9,6 +9,7 @@ from pysmartthings.device import Status from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform @@ -104,4 +105,4 @@ async def test_unload_config_entry(hass, device_factory): # Act await hass.config_entries.async_forward_entry_unload(config_entry, "lock") # Assert - assert not hass.states.get("lock.lock_1") + assert hass.states.get("lock.lock_1").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py index a9e6443d2bf..6ab4bc08080 100644 --- a/tests/components/smartthings/test_scene.py +++ b/tests/components/smartthings/test_scene.py @@ -5,7 +5,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_UNAVAILABLE from .conftest import setup_platform @@ -46,4 +46,4 @@ async def test_unload_config_entry(hass, scene): # Act await hass.config_entries.async_forward_entry_unload(config_entry, SCENE_DOMAIN) # Assert - assert not hass.states.get("scene.test_scene") + assert hass.states.get("scene.test_scene").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 3faf0f621a3..53f4b2c7244 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -117,4 +118,4 @@ async def test_unload_config_entry(hass, device_factory): # Act await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") # Assert - assert not hass.states.get("sensor.sensor_1_battery") + assert hass.states.get("sensor.sensor_1_battery").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 3ac86426eeb..27ed5050bee 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -12,6 +12,7 @@ from homeassistant.components.switch import ( ATTR_TODAY_ENERGY_KWH, DOMAIN as SWITCH_DOMAIN, ) +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform @@ -96,4 +97,4 @@ async def test_unload_config_entry(hass, device_factory): # Act await hass.config_entries.async_forward_entry_unload(config_entry, "switch") # Assert - assert not hass.states.get("switch.switch_1") + assert hass.states.get("switch.switch_1").state == STATE_UNAVAILABLE diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py index cd611662597..b223202d5b1 100644 --- a/tests/components/vizio/test_init.py +++ b/tests/components/vizio/test_init.py @@ -3,6 +3,7 @@ import pytest from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN from homeassistant.components.vizio.const import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component @@ -41,7 +42,10 @@ async def test_tv_load_and_unload( assert await config_entry.async_unload(hass) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 + entities = hass.states.async_entity_ids(MP_DOMAIN) + assert len(entities) == 1 + for entity in entities: + assert hass.states.get(entity).state == STATE_UNAVAILABLE assert DOMAIN not in hass.data @@ -62,5 +66,8 @@ async def test_speaker_load_and_unload( assert await config_entry.async_unload(hass) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 + entities = hass.states.async_entity_ids(MP_DOMAIN) + assert len(entities) == 1 + for entity in entities: + assert hass.states.get(entity).state == STATE_UNAVAILABLE assert DOMAIN not in hass.data diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index c91ae33d986..05a0bd0d8d4 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -11,7 +11,7 @@ from homeassistant.components.yeelight import ( DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, ) -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component @@ -50,6 +50,12 @@ async def test_setup_discovery(hass: HomeAssistant): # Unload assert await hass.config_entries.async_unload(config_entry.entry_id) + assert hass.states.get(ENTITY_BINARY_SENSOR).state == STATE_UNAVAILABLE + assert hass.states.get(ENTITY_LIGHT).state == STATE_UNAVAILABLE + + # Remove + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert hass.states.get(ENTITY_BINARY_SENSOR) is None assert hass.states.get(ENTITY_LIGHT) is None diff --git a/tests/helpers/test_collection.py b/tests/helpers/test_collection.py index d5a8526b6da..11ab0f46ce4 100644 --- a/tests/helpers/test_collection.py +++ b/tests/helpers/test_collection.py @@ -226,7 +226,7 @@ async def test_attach_entity_component_collection(hass): """Test attaching collection to entity component.""" ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass) coll = collection.ObservableCollection(_LOGGER) - collection.attach_entity_component_collection(ent_comp, coll, MockEntity) + collection.sync_entity_lifecycle(hass, "test", "test", ent_comp, coll, MockEntity) await coll.notify_changes( [ diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 52149b060e4..b8d0fc7dc9c 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest -from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import Context from homeassistant.helpers import entity, entity_registry @@ -718,3 +718,29 @@ async def test_setup_source(hass): await platform.async_reset() assert entity.entity_sources(hass) == {} + + +async def test_removing_entity_unavailable(hass): + """Test removing an entity that is still registered creates an unavailable state.""" + entry = entity_registry.RegistryEntry( + entity_id="hello.world", + unique_id="test-unique-id", + platform="test-platform", + disabled_by=None, + ) + + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "hello.world" + ent.registry_entry = entry + ent.async_write_ha_state() + + state = hass.states.get("hello.world") + assert state is not None + assert state.state == STATE_UNKNOWN + + await ent.async_remove() + + state = hass.states.get("hello.world") + assert state is not None + assert state.state == STATE_UNAVAILABLE