Don't allow entities without platform

This commit is contained in:
G Johansson 2024-12-06 16:18:54 +00:00 committed by Franck Nijhof
parent 2295e3779a
commit 43fbb2ab7b
2 changed files with 97 additions and 77 deletions

View File

@ -64,7 +64,7 @@ from .event import (
async_track_device_registry_updated_event, async_track_device_registry_updated_event,
async_track_entity_registry_updated_event, async_track_entity_registry_updated_event,
) )
from .frame import report_non_thread_safe_operation from .frame import ReportBehavior, report_non_thread_safe_operation, report_usage
from .typing import UNDEFINED, StateType, UndefinedType from .typing import UNDEFINED, StateType, UndefinedType
timer = time.time timer = time.time
@ -464,9 +464,6 @@ class Entity(
# it should be using async_write_ha_state. # it should be using async_write_ha_state.
_async_update_ha_state_reported = False _async_update_ha_state_reported = False
# If we reported this entity was added without its platform set
_no_platform_reported = False
# If we reported the name translation placeholders do not match the name # If we reported the name translation placeholders do not match the name
_name_translation_placeholders_reported = False _name_translation_placeholders_reported = False
@ -722,9 +719,6 @@ class Entity(
# value. # value.
type.__getattribute__(self.__class__, "name") type.__getattribute__(self.__class__, "name")
is type.__getattribute__(Entity, "name") is type.__getattribute__(Entity, "name")
# The check for self.platform guards against integrations not using an
# EntityComponent and can be removed in HA Core 2024.1
and self.platform
): ):
name = self._name_internal( name = self._name_internal(
self._object_id_device_class_name, self._object_id_device_class_name,
@ -737,10 +731,6 @@ class Entity(
@cached_property @cached_property
def name(self) -> str | UndefinedType | None: def name(self) -> str | UndefinedType | None:
"""Return the name of the entity.""" """Return the name of the entity."""
# The check for self.platform guards against integrations not using an
# EntityComponent and can be removed in HA Core 2024.1
if not self.platform:
return self._name_internal(None, {})
return self._name_internal( return self._name_internal(
self._device_class_name, self._device_class_name,
self.platform.platform_translations, self.platform.platform_translations,
@ -983,21 +973,13 @@ class Entity(
if self.hass is None: if self.hass is None:
raise RuntimeError(f"Attribute hass is None for {self}") raise RuntimeError(f"Attribute hass is None for {self}")
# The check for self.platform guards against integrations not using an # Break if entity is not loaded using EntityComponent, introduced in 2025.1
# EntityComponent and can be removed in HA Core 2024.1 if self.platform is None:
if self.platform is None and not self._no_platform_reported: # type: ignore[unreachable] report_usage( # type: ignore[unreachable]
report_issue = self._suggest_report_issue() # type: ignore[unreachable] f"Entity {self.entity_id} ({type(self)}) does not have a platform,"
_LOGGER.warning( "this may be caused by adding it manually instead of with an EntityComponent helper",
( core_behavior=ReportBehavior.ERROR,
"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: if self.entity_id is None:
raise NoEntitySpecifiedError( raise NoEntitySpecifiedError(
@ -1499,10 +1481,7 @@ class Entity(
Not to be extended by integrations. Not to be extended by integrations.
""" """
# The check for self.platform guards against integrations not using an del entity_sources(self.hass)[self.entity_id]
# EntityComponent and can be removed in HA Core 2024.1
if self.platform:
del entity_sources(self.hass)[self.entity_id]
@callback @callback
def _async_registry_updated( def _async_registry_updated(
@ -1632,9 +1611,7 @@ class Entity(
def _suggest_report_issue(self) -> str: def _suggest_report_issue(self) -> str:
"""Suggest to report an issue.""" """Suggest to report an issue."""
# The check for self.platform guards against integrations not using an platform_name = self.platform.platform_name
# EntityComponent and can be removed in HA Core 2024.1
platform_name = self.platform.platform_name if self.platform else None
return async_suggest_report_issue( return async_suggest_report_issue(
self.hass, integration_domain=platform_name, module=type(self).__module__ self.hass, integration_domain=platform_name, module=type(self).__module__
) )

View File

@ -6,6 +6,7 @@ import dataclasses
from datetime import timedelta from datetime import timedelta
from enum import IntFlag from enum import IntFlag
import logging import logging
import re
import threading import threading
from typing import Any from typing import Any
from unittest.mock import MagicMock, PropertyMock, patch from unittest.mock import MagicMock, PropertyMock, patch
@ -100,6 +101,8 @@ async def test_async_update_support(hass: HomeAssistant) -> None:
ent = AsyncEntity() ent = AsyncEntity()
ent.hass = hass ent.hass = hass
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent])
await ent.async_update_ha_state(True) await ent.async_update_ha_state(True)
@ -124,6 +127,8 @@ async def test_device_class(hass: HomeAssistant) -> None:
ent = entity.Entity() ent = entity.Entity()
ent.entity_id = "test.overwrite_hidden_true" ent.entity_id = "test.overwrite_hidden_true"
ent.hass = hass ent.hass = hass
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent])
ent.async_write_ha_state() ent.async_write_ha_state()
state = hass.states.get(ent.entity_id) state = hass.states.get(ent.entity_id)
assert state.attributes.get(ATTR_DEVICE_CLASS) is None assert state.attributes.get(ATTR_DEVICE_CLASS) is None
@ -151,6 +156,9 @@ async def test_warn_slow_update(
mock_entity.entity_id = "comp_test.test_entity" mock_entity.entity_id = "comp_test.test_entity"
mock_entity.async_update = async_update mock_entity.async_update = async_update
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([mock_entity])
fast_update_time = 0.0000001 fast_update_time = 0.0000001
with patch.object(entity, "SLOW_UPDATE_WARNING", fast_update_time): with patch.object(entity, "SLOW_UPDATE_WARNING", fast_update_time):
@ -341,6 +349,9 @@ async def test_async_parallel_updates_with_zero(hass: HomeAssistant) -> None:
ent_1 = AsyncEntity("sensor.test_1", 1) ent_1 = AsyncEntity("sensor.test_1", 1)
ent_2 = AsyncEntity("sensor.test_2", 2) ent_2 = AsyncEntity("sensor.test_2", 2)
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent_1, ent_2])
try: try:
ent_1.async_schedule_update_ha_state(True) ent_1.async_schedule_update_ha_state(True)
ent_2.async_schedule_update_ha_state(True) ent_2.async_schedule_update_ha_state(True)
@ -382,6 +393,9 @@ async def test_async_parallel_updates_with_zero_on_sync_update(
ent_1 = AsyncEntity("sensor.test_1", 1) ent_1 = AsyncEntity("sensor.test_1", 1)
ent_2 = AsyncEntity("sensor.test_2", 2) ent_2 = AsyncEntity("sensor.test_2", 2)
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent_1, ent_2])
try: try:
ent_1.async_schedule_update_ha_state(True) ent_1.async_schedule_update_ha_state(True)
ent_2.async_schedule_update_ha_state(True) ent_2.async_schedule_update_ha_state(True)
@ -412,7 +426,6 @@ async def test_async_parallel_updates_with_one(hass: HomeAssistant) -> None:
self.entity_id = entity_id self.entity_id = entity_id
self.hass = hass self.hass = hass
self._count = count self._count = count
self.parallel_updates = test_semaphore
async def async_update(self) -> None: async def async_update(self) -> None:
"""Test update.""" """Test update."""
@ -423,6 +436,10 @@ async def test_async_parallel_updates_with_one(hass: HomeAssistant) -> None:
ent_2 = AsyncEntity("sensor.test_2", 2) ent_2 = AsyncEntity("sensor.test_2", 2)
ent_3 = AsyncEntity("sensor.test_3", 3) ent_3 = AsyncEntity("sensor.test_3", 3)
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
platform.parallel_updates = test_semaphore
await platform.async_add_entities([ent_1, ent_2, ent_3])
await test_lock.acquire() await test_lock.acquire()
try: try:
@ -488,7 +505,6 @@ async def test_async_parallel_updates_with_two(hass: HomeAssistant) -> None:
self.entity_id = entity_id self.entity_id = entity_id
self.hass = hass self.hass = hass
self._count = count self._count = count
self.parallel_updates = test_semaphore
async def async_update(self) -> None: async def async_update(self) -> None:
"""Test update.""" """Test update."""
@ -500,6 +516,10 @@ async def test_async_parallel_updates_with_two(hass: HomeAssistant) -> None:
ent_3 = AsyncEntity("sensor.test_3", 3) ent_3 = AsyncEntity("sensor.test_3", 3)
ent_4 = AsyncEntity("sensor.test_4", 4) ent_4 = AsyncEntity("sensor.test_4", 4)
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
platform.parallel_updates = test_semaphore
await platform.async_add_entities([ent_1, ent_2, ent_3, ent_4])
await test_lock.acquire() await test_lock.acquire()
try: try:
@ -557,7 +577,6 @@ async def test_async_parallel_updates_with_one_using_executor(
"""Initialize sync test entity.""" """Initialize sync test entity."""
self.entity_id = entity_id self.entity_id = entity_id
self.hass = hass self.hass = hass
self.parallel_updates = test_semaphore
def update(self) -> None: def update(self) -> None:
"""Test update.""" """Test update."""
@ -565,6 +584,10 @@ async def test_async_parallel_updates_with_one_using_executor(
entities = [SyncEntity(f"sensor.test_{i}") for i in range(3)] entities = [SyncEntity(f"sensor.test_{i}") for i in range(3)]
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
platform.parallel_updates = test_semaphore
await platform.async_add_entities(entities)
await asyncio.gather( await asyncio.gather(
*[ *[
hass.async_create_task( hass.async_create_task(
@ -583,6 +606,8 @@ async def test_async_remove_no_platform(hass: HomeAssistant) -> None:
ent = entity.Entity() ent = entity.Entity()
ent.hass = hass ent.hass = hass
ent.entity_id = "test.test" ent.entity_id = "test.test"
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent])
ent.async_write_ha_state() ent.async_write_ha_state()
assert len(hass.states.async_entity_ids()) == 1 assert len(hass.states.async_entity_ids()) == 1
await ent.async_remove() await ent.async_remove()
@ -659,6 +684,8 @@ async def test_set_context(hass: HomeAssistant) -> None:
ent.hass = hass ent.hass = hass
ent.entity_id = "hello.world" ent.entity_id = "hello.world"
ent.async_set_context(context) ent.async_set_context(context)
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent])
ent.async_write_ha_state() ent.async_write_ha_state()
assert hass.states.get("hello.world").context == context assert hass.states.get("hello.world").context == context
@ -671,6 +698,8 @@ async def test_set_context_expired(hass: HomeAssistant) -> None:
ent = entity.Entity() ent = entity.Entity()
ent.hass = hass ent.hass = hass
ent.entity_id = "hello.world" ent.entity_id = "hello.world"
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent])
ent.async_set_context(context) ent.async_set_context(context)
ent.async_write_ha_state() ent.async_write_ha_state()
@ -757,6 +786,8 @@ async def test_capability_attrs(hass: HomeAssistant) -> None:
ent = entity.Entity() ent = entity.Entity()
ent.hass = hass ent.hass = hass
ent.entity_id = "hello.world" ent.entity_id = "hello.world"
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent])
ent.async_write_ha_state() ent.async_write_ha_state()
state = hass.states.get("hello.world") state = hass.states.get("hello.world")
@ -896,6 +927,8 @@ async def test_float_conversion(hass: HomeAssistant) -> None:
ent = entity.Entity() ent = entity.Entity()
ent.hass = hass ent.hass = hass
ent.entity_id = "hello.world" ent.entity_id = "hello.world"
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent])
ent.async_write_ha_state() ent.async_write_ha_state()
state = hass.states.get("hello.world") state = hass.states.get("hello.world")
@ -910,6 +943,9 @@ async def test_attribution_attribute(hass: HomeAssistant) -> None:
mock_entity.entity_id = "hello.world" mock_entity.entity_id = "hello.world"
mock_entity._attr_attribution = "Home Assistant" mock_entity._attr_attribution = "Home Assistant"
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([mock_entity])
mock_entity.async_schedule_update_ha_state(True) mock_entity.async_schedule_update_ha_state(True)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -963,12 +999,15 @@ def test_entity_category_schema_error(value) -> None:
schema(value) schema(value)
async def test_entity_description_fallback() -> None: async def test_entity_description_fallback(hass: HomeAssistant) -> None:
"""Test entity description has same defaults as entity.""" """Test entity description has same defaults as entity."""
ent = entity.Entity() ent = entity.Entity()
ent_with_description = entity.Entity() ent_with_description = entity.Entity()
ent_with_description.entity_description = entity.EntityDescription(key="test") ent_with_description.entity_description = entity.EntityDescription(key="test")
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent, ent_with_description])
for field in dataclasses.fields(entity.EntityDescription._dataclass): for field in dataclasses.fields(entity.EntityDescription._dataclass):
if field.name == "key": if field.name == "key":
continue continue
@ -1665,31 +1704,23 @@ async def test_warn_using_async_update_ha_state(
assert error_message not in caplog.text assert error_message not in caplog.text
async def test_warn_no_platform( async def test_raise_if_no_platform(hass: HomeAssistant) -> None:
hass: HomeAssistant, caplog: pytest.LogCaptureFixture """Test we raise if entity does not have a platform."""
) -> None:
"""Test we warn am entity does not have a platform."""
ent = entity.Entity() ent = entity.Entity()
ent.hass = hass ent.hass = hass
ent.platform = MockEntityPlatform(hass)
ent.entity_id = "hello.world" ent.entity_id = "hello.world"
error_message = "does not have a platform"
# Without a platform, it should trigger the warning # Without a platform, it should raise
ent.platform = None error = re.escape(
caplog.clear() "Detected code that Entity hello.world (<class 'homeassistant.helpers.entity.Entity'>)"
ent.async_write_ha_state() " does not have a platform,this may be caused by adding it manually instead"
assert error_message in caplog.text " of with an EntityComponent helper. Please report this issue"
)
# Without a platform, it should not trigger the warning again with pytest.raises(
caplog.clear() RuntimeError,
ent.async_write_ha_state() match=error,
assert error_message not in caplog.text ):
ent.async_write_ha_state()
# No warning if the entity has a platform
caplog.clear()
ent.async_write_ha_state()
assert error_message not in caplog.text
async def test_invalid_state( async def test_invalid_state(
@ -1700,6 +1731,9 @@ async def test_invalid_state(
ent.entity_id = "test.test" ent.entity_id = "test.test"
ent.hass = hass ent.hass = hass
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent])
ent._attr_state = "x" * 255 ent._attr_state = "x" * 255
ent.async_write_ha_state() ent.async_write_ha_state()
assert hass.states.get("test.test").state == "x" * 255 assert hass.states.get("test.test").state == "x" * 255
@ -1726,13 +1760,6 @@ async def test_suggest_report_issue_built_in(
mock_entity = entity.Entity() mock_entity = entity.Entity()
mock_entity.entity_id = "comp_test.test_entity" mock_entity.entity_id = "comp_test.test_entity"
suggestion = mock_entity._suggest_report_issue()
assert suggestion == (
"create a bug report at https://github.com/home-assistant/core/issues"
"?q=is%3Aopen+is%3Aissue"
)
mock_integration(hass, MockModule(domain="test"), built_in=True)
platform = MockEntityPlatform(hass, domain="comp_test", platform_name="test") platform = MockEntityPlatform(hass, domain="comp_test", platform_name="test")
await platform.async_add_entities([mock_entity]) await platform.async_add_entities([mock_entity])
@ -1756,20 +1783,27 @@ async def test_suggest_report_issue_custom_component(
mock_entity = CustomComponentEntity() mock_entity = CustomComponentEntity()
mock_entity.entity_id = "comp_test.test_entity" mock_entity.entity_id = "comp_test.test_entity"
suggestion = mock_entity._suggest_report_issue()
assert suggestion == "report it to the custom integration author"
mock_integration(
hass,
MockModule(
domain="test", partial_manifest={"issue_tracker": "https://some_url"}
),
built_in=False,
)
platform = MockEntityPlatform(hass, domain="comp_test", platform_name="test") platform = MockEntityPlatform(hass, domain="comp_test", platform_name="test")
await platform.async_add_entities([mock_entity]) await platform.async_add_entities([mock_entity])
suggestion = mock_entity._suggest_report_issue() suggestion = mock_entity._suggest_report_issue()
assert suggestion == "report it to the author of the 'test' custom integration"
mock_entity2 = CustomComponentEntity()
mock_entity2.entity_id = "comp_test.test_entity2"
mock_integration(
hass,
MockModule(
domain="test2", partial_manifest={"issue_tracker": "https://some_url"}
),
built_in=False,
)
platform = MockEntityPlatform(hass, domain="comp_test2", platform_name="test2")
await platform.async_add_entities([mock_entity2])
await hass.async_block_till_done()
suggestion = mock_entity2._suggest_report_issue()
assert suggestion == "create a bug report at https://some_url" assert suggestion == "create a bug report at https://some_url"
@ -2629,18 +2663,27 @@ async def test_async_write_ha_state_thread_safety(hass: HomeAssistant) -> None:
ent = entity.Entity() ent = entity.Entity()
ent.entity_id = "test.any" ent.entity_id = "test.any"
ent.hass = hass ent.hass = hass
ent.async_write_ha_state()
assert hass.states.get(ent.entity_id)
ent2 = entity.Entity() ent2 = entity.Entity()
ent2.entity_id = "test.any2" ent2.entity_id = "test.any2"
ent2.hass = hass ent2.hass = hass
platform = MockEntityPlatform(hass, domain="test")
await platform.async_add_entities([ent, ent2])
ent._attr_state = "test"
ent2._attr_state = "test"
ent.async_write_ha_state()
assert hass.states.get(ent.entity_id).state == "test"
with pytest.raises( with pytest.raises(
RuntimeError, RuntimeError,
match="Detected code that calls async_write_ha_state from a thread.", match="Detected code that calls async_write_ha_state from a thread.",
): ):
await hass.async_add_executor_job(ent2.async_write_ha_state) await hass.async_add_executor_job(ent2.async_write_ha_state)
assert not hass.states.get(ent2.entity_id) await hass.async_block_till_done()
assert hass.states.get(ent2.entity_id).state != "test"
async def test_async_write_ha_state_thread_safety_always( async def test_async_write_ha_state_thread_safety_always(