1
mirror of https://github.com/home-assistant/core synced 2024-10-04 07:58:43 +02:00

Remove unneeded checks for Entity.platform (#94321)

* Remove unneeded checks for Entity.platform

* Update tests

* Prevent breaking integrations without an EntityComponent

* Warn when entity has no platform
This commit is contained in:
Erik Montnemery 2023-06-09 15:17:41 +02:00 committed by GitHub
parent 239e2d9820
commit 59f5b8f2d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 95 additions and 44 deletions

View File

@ -373,7 +373,6 @@ class ScannerEntity(BaseTrackerEntity):
# Entities without a unique ID don't have a device
if (
not self.registry_entry
or not self.platform
or not self.platform.config_entry
or not self.mac_address
or (device_entry := self.find_device_entry()) is None

View File

@ -247,11 +247,7 @@ class MeteoFranceSensor(CoordinatorEntity[DataUpdateCoordinator[_DataT]], Sensor
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
assert (
self.platform
and self.platform.config_entry
and self.platform.config_entry.unique_id
)
assert self.platform.config_entry and self.platform.config_entry.unique_id
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self.platform.config_entry.unique_id)},

View File

@ -109,11 +109,7 @@ class MeteoFranceWeather(
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
assert (
self.platform
and self.platform.config_entry
and self.platform.config_entry.unique_id
)
assert self.platform.config_entry and self.platform.config_entry.unique_id
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self.platform.config_entry.unique_id)},

View File

@ -227,7 +227,6 @@ class MySensorsEntity(MySensorsDevice, Entity):
"""Return entity specific state attributes."""
attr = self._extra_attributes
assert self.platform
assert self.platform.config_entry
attr[ATTR_DEVICE] = self.platform.config_entry.data[CONF_DEVICE]

View File

@ -128,7 +128,6 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C
_LOGGER.debug("Update stream URL for camera %s", self.camera_data.name)
self.stream.update_source(url)
assert self.platform
assert self.platform.config_entry
self.async_on_remove(
async_dispatcher_connect(

View File

@ -165,7 +165,6 @@ class TTSMediaSource(MediaSource):
raise BrowseError("Unknown provider")
if isinstance(engine_instance, TextToSpeechEntity):
assert engine_instance.platform is not None
engine_domain = engine_instance.platform.domain
else:
engine_domain = engine

View File

@ -622,7 +622,7 @@ class BaseLight(LogMixin, light.LightEntity):
)
if self._debounced_member_refresh is not None:
self.debug("transition complete - refreshing group member states")
assert self.platform and self.platform.config_entry
assert self.platform.config_entry
self.platform.config_entry.async_create_background_task(
self.hass,
self._debounced_member_refresh.async_call(),

View File

@ -258,6 +258,9 @@ class Entity(ABC):
# it should be using async_write_ha_state.
_async_update_ha_state_reported = False
# If we reported this entity was added without its platform set
_no_platform_reported = False
# Protect for multiple updates
_update_staged = False
@ -331,7 +334,6 @@ class Entity(ABC):
if hasattr(self, "_attr_name"):
return self._attr_name
if self.translation_key is not None and self.has_entity_name:
assert self.platform
name_translation_key = (
f"component.{self.platform.platform_name}.entity.{self.platform.domain}"
f".{self.translation_key}.name"
@ -584,6 +586,22 @@ class Entity(ABC):
if self.hass is None:
raise RuntimeError(f"Attribute hass is None for {self}")
# The check for self.platform guards against integrations not using an
# EntityComponent and can be removed in HA Core 2024.1
if self.platform is None and not self._no_platform_reported: # type: ignore[unreachable]
report_issue = self._suggest_report_issue() # type: ignore[unreachable]
_LOGGER.warning(
(
"Entity %s (%s) does not have a platform, this may be caused by "
"adding it manually instead of with an EntityComponent helper"
", please %s"
),
self.entity_id,
type(self),
report_issue,
)
self._no_platform_reported = True
if self.entity_id is None:
raise NoEntitySpecifiedError(
f"No entity id specified for entity {self.name}"
@ -636,7 +654,6 @@ class Entity(ABC):
if entry and entry.disabled_by:
if not self._disabled_reported:
self._disabled_reported = True
assert self.platform is not None
_LOGGER.warning(
(
"Entity %s is incorrectly being triggered for updates while it"
@ -861,6 +878,8 @@ class Entity(ABC):
If the entity doesn't have a non disabled entry in the entity registry,
or if force_remove=True, its state will be removed.
"""
# The check for self.platform guards against integrations not using an
# EntityComponent and can be removed in HA Core 2024.1
if self.platform and self._platform_state != EntityPlatformState.ADDED:
raise HomeAssistantError(
f"Entity {self.entity_id} async_remove called twice"
@ -908,19 +927,18 @@ class Entity(ABC):
Not to be extended by integrations.
"""
if self.platform:
info = {
"domain": self.platform.platform_name,
"custom_component": "custom_components" in type(self).__module__,
}
info = {
"domain": self.platform.platform_name,
"custom_component": "custom_components" in type(self).__module__,
}
if self.platform.config_entry:
info["source"] = SOURCE_CONFIG_ENTRY
info["config_entry"] = self.platform.config_entry.entry_id
else:
info["source"] = SOURCE_PLATFORM_CONFIG
if self.platform.config_entry:
info["source"] = SOURCE_CONFIG_ENTRY
info["config_entry"] = self.platform.config_entry.entry_id
else:
info["source"] = SOURCE_PLATFORM_CONFIG
self.hass.data[DATA_ENTITY_SOURCE][self.entity_id] = info
self.hass.data[DATA_ENTITY_SOURCE][self.entity_id] = info
if self.registry_entry is not None:
# This is an assert as it should never happen, but helps in tests
@ -940,6 +958,8 @@ class Entity(ABC):
Not to be extended by integrations.
"""
# The check for self.platform guards against integrations not using an
# EntityComponent and can be removed in HA Core 2024.1
if self.platform:
self.hass.data[DATA_ENTITY_SOURCE].pop(self.entity_id)
@ -974,7 +994,6 @@ class Entity(ABC):
await self.async_remove(force_remove=True)
assert self.platform is not None
self.entity_id = self.registry_entry.entity_id
await self.platform.async_add_entities([self])
@ -1048,6 +1067,8 @@ class Entity(ABC):
"create a bug report at "
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue"
)
# The check for self.platform guards against integrations not using an
# EntityComponent and can be removed in HA Core 2024.1
if self.platform:
report_issue += (
f"+label%3A%22integration%3A+{self.platform.platform_name}%22"

View File

@ -9,7 +9,7 @@ from homeassistant.components.arcam_fmj.const import DEFAULT_NAME
from homeassistant.components.arcam_fmj.media_player import ArcamFmj
from homeassistant.const import CONF_HOST, CONF_PORT
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, MockEntityPlatform
MOCK_HOST = "127.0.0.1"
MOCK_PORT = 50000
@ -75,6 +75,7 @@ def player_fixture(hass, state):
player = ArcamFmj(MOCK_NAME, state, MOCK_UUID)
player.entity_id = MOCK_ENTITY_ID
player.hass = hass
player.platform = MockEntityPlatform(hass)
player.async_write_ha_state = Mock()
return player

View File

@ -35,6 +35,7 @@ from homeassistant.setup import async_setup_component
from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
from tests.common import (
MockEntityPlatform,
async_mock_restore_state_shutdown_restart,
mock_restore_cache_with_extra_data,
)
@ -246,6 +247,7 @@ async def test_deprecation_warnings(
"""Test overriding the deprecated attributes is possible and warnings are logged."""
number = MockDefaultNumberEntityDeprecated()
number.hass = hass
number.platform = MockEntityPlatform(hass)
assert number.max_value == 100.0
assert number.min_value == 0.0
assert number.step == 1.0
@ -254,6 +256,7 @@ async def test_deprecation_warnings(
number_2 = MockNumberEntityDeprecated()
number_2.hass = hass
number_2.platform = MockEntityPlatform(hass)
assert number_2.max_value == 0.5
assert number_2.min_value == -0.5
assert number_2.step == 0.1
@ -262,6 +265,7 @@ async def test_deprecation_warnings(
number_3 = MockNumberEntityAttrDeprecated()
number_3.hass = hass
number_3.platform = MockEntityPlatform(hass)
assert number_3.max_value == 1000.0
assert number_3.min_value == -1000.0
assert number_3.step == 100.0
@ -270,6 +274,7 @@ async def test_deprecation_warnings(
number_4 = MockNumberEntityDescrDeprecated()
number_4.hass = hass
number_4.platform = MockEntityPlatform(hass)
assert number_4.max_value == 10.0
assert number_4.min_value == -10.0
assert number_4.step == 2.0

View File

@ -578,12 +578,14 @@ async def test_async_remove_no_platform(hass: HomeAssistant) -> None:
async def test_async_remove_runs_callbacks(hass: HomeAssistant) -> None:
"""Test async_remove method when no platform set."""
"""Test async_remove runs on_remove callback."""
result = []
platform = MockEntityPlatform(hass, domain="test")
ent = entity.Entity()
ent.hass = hass
ent.entity_id = "test.test"
await platform.async_add_entities([ent])
ent.async_on_remove(lambda: result.append(1))
await ent.async_remove()
assert len(result) == 1
@ -593,11 +595,12 @@ async def test_async_remove_ignores_in_flight_polling(hass: HomeAssistant) -> No
"""Test in flight polling is ignored after removing."""
result = []
platform = MockEntityPlatform(hass, domain="test")
ent = entity.Entity()
ent.hass = hass
ent.entity_id = "test.test"
ent.async_on_remove(lambda: result.append(1))
ent.async_write_ha_state()
await platform.async_add_entities([ent])
assert hass.states.get("test.test").state == STATE_UNKNOWN
await ent.async_remove()
assert len(result) == 1
@ -798,18 +801,18 @@ async def test_setup_source(hass: HomeAssistant) -> None:
async def test_removing_entity_unavailable(hass: HomeAssistant) -> None:
"""Test removing an entity that is still registered creates an unavailable state."""
entry = er.RegistryEntry(
er.RegistryEntry(
entity_id="hello.world",
unique_id="test-unique-id",
platform="test-platform",
disabled_by=None,
)
platform = MockEntityPlatform(hass, domain="hello")
ent = entity.Entity()
ent.hass = hass
ent.entity_id = "hello.world"
ent.registry_entry = entry
ent.async_write_ha_state()
ent._attr_unique_id = "test-unique-id"
await platform.async_add_entities([ent])
state = hass.states.get("hello.world")
assert state is not None
@ -1112,19 +1115,48 @@ async def test_warn_using_async_update_ha_state(
"""Test we warn once when using async_update_ha_state without force_update."""
ent = entity.Entity()
ent.hass = hass
ent.platform = MockEntityPlatform(hass)
ent.entity_id = "hello.world"
error_message = "is using self.async_update_ha_state()"
# When forcing, it should not trigger the warning
caplog.clear()
await ent.async_update_ha_state(force_refresh=True)
assert "is using self.async_update_ha_state()" not in caplog.text
assert error_message not in caplog.text
# When not forcing, it should trigger the warning
caplog.clear()
await ent.async_update_ha_state()
assert "is using self.async_update_ha_state()" in caplog.text
assert error_message in caplog.text
# When not forcing, it should not trigger the warning again
caplog.clear()
await ent.async_update_ha_state()
assert "is using self.async_update_ha_state()" not in caplog.text
assert error_message not in caplog.text
async def test_warn_no_platform(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test we warn am entity does not have a platform."""
ent = entity.Entity()
ent.hass = hass
ent.platform = MockEntityPlatform(hass)
ent.entity_id = "hello.world"
error_message = "does not have a platform"
# No warning if the entity has a platform
caplog.clear()
ent.async_write_ha_state()
assert error_message not in caplog.text
# Without a platform, it should trigger the warning
ent.platform = None
caplog.clear()
ent.async_write_ha_state()
assert error_message in caplog.text
# Without a platform, it should not trigger the warning again
caplog.clear()
ent.async_write_ha_state()
assert error_message not in caplog.text

View File

@ -27,6 +27,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from tests.common import (
MockEntityPlatform,
MockModule,
MockPlatform,
async_fire_time_changed,
@ -266,15 +267,16 @@ async def test_dump_data(hass: HomeAssistant) -> None:
State("input_boolean.b5", "unavailable", {"restored": True}),
]
platform = MockEntityPlatform(hass, domain="input_boolean")
entity = Entity()
entity.hass = hass
entity.entity_id = "input_boolean.b0"
await entity.async_internal_added_to_hass()
await platform.async_add_entities([entity])
entity = RestoreEntity()
entity.hass = hass
entity.entity_id = "input_boolean.b1"
await entity.async_internal_added_to_hass()
await platform.async_add_entities([entity])
data = async_get(hass)
now = dt_util.utcnow()
@ -340,15 +342,16 @@ async def test_dump_error(hass: HomeAssistant) -> None:
State("input_boolean.b2", "on"),
]
platform = MockEntityPlatform(hass, domain="input_boolean")
entity = Entity()
entity.hass = hass
entity.entity_id = "input_boolean.b0"
await entity.async_internal_added_to_hass()
await platform.async_add_entities([entity])
entity = RestoreEntity()
entity.hass = hass
entity.entity_id = "input_boolean.b1"
await entity.async_internal_added_to_hass()
await platform.async_add_entities([entity])
data = async_get(hass)
@ -378,10 +381,11 @@ async def test_load_error(hass: HomeAssistant) -> None:
async def test_state_saved_on_remove(hass: HomeAssistant) -> None:
"""Test that we save entity state on removal."""
platform = MockEntityPlatform(hass, domain="input_boolean")
entity = RestoreEntity()
entity.hass = hass
entity.entity_id = "input_boolean.b0"
await entity.async_internal_added_to_hass()
await platform.async_add_entities([entity])
now = dt_util.utcnow()
hass.states.async_set(