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

View File

@ -6,6 +6,7 @@ import dataclasses
from datetime import timedelta
from enum import IntFlag
import logging
import re
import threading
from typing import Any
from unittest.mock import MagicMock, PropertyMock, patch
@ -100,6 +101,8 @@ async def test_async_update_support(hass: HomeAssistant) -> None:
ent = AsyncEntity()
ent.hass = hass
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent])
await ent.async_update_ha_state(True)
@ -124,6 +127,8 @@ async def test_device_class(hass: HomeAssistant) -> None:
ent = entity.Entity()
ent.entity_id = "test.overwrite_hidden_true"
ent.hass = hass
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent])
ent.async_write_ha_state()
state = hass.states.get(ent.entity_id)
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.async_update = async_update
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([mock_entity])
fast_update_time = 0.0000001
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_2 = AsyncEntity("sensor.test_2", 2)
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent_1, ent_2])
try:
ent_1.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_2 = AsyncEntity("sensor.test_2", 2)
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent_1, ent_2])
try:
ent_1.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.hass = hass
self._count = count
self.parallel_updates = test_semaphore
async def async_update(self) -> None:
"""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_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()
try:
@ -488,7 +505,6 @@ async def test_async_parallel_updates_with_two(hass: HomeAssistant) -> None:
self.entity_id = entity_id
self.hass = hass
self._count = count
self.parallel_updates = test_semaphore
async def async_update(self) -> None:
"""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_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()
try:
@ -557,7 +577,6 @@ async def test_async_parallel_updates_with_one_using_executor(
"""Initialize sync test entity."""
self.entity_id = entity_id
self.hass = hass
self.parallel_updates = test_semaphore
def update(self) -> None:
"""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)]
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
platform.parallel_updates = test_semaphore
await platform.async_add_entities(entities)
await asyncio.gather(
*[
hass.async_create_task(
@ -583,6 +606,8 @@ async def test_async_remove_no_platform(hass: HomeAssistant) -> None:
ent = entity.Entity()
ent.hass = hass
ent.entity_id = "test.test"
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent])
ent.async_write_ha_state()
assert len(hass.states.async_entity_ids()) == 1
await ent.async_remove()
@ -659,6 +684,8 @@ async def test_set_context(hass: HomeAssistant) -> None:
ent.hass = hass
ent.entity_id = "hello.world"
ent.async_set_context(context)
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent])
ent.async_write_ha_state()
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.hass = hass
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_write_ha_state()
@ -757,6 +786,8 @@ async def test_capability_attrs(hass: HomeAssistant) -> None:
ent = entity.Entity()
ent.hass = hass
ent.entity_id = "hello.world"
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent])
ent.async_write_ha_state()
state = hass.states.get("hello.world")
@ -896,6 +927,8 @@ async def test_float_conversion(hass: HomeAssistant) -> None:
ent = entity.Entity()
ent.hass = hass
ent.entity_id = "hello.world"
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent])
ent.async_write_ha_state()
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._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)
await hass.async_block_till_done()
@ -963,12 +999,15 @@ def test_entity_category_schema_error(value) -> None:
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."""
ent = entity.Entity()
ent_with_description = entity.Entity()
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):
if field.name == "key":
continue
@ -1665,31 +1704,23 @@ async def test_warn_using_async_update_ha_state(
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."""
async def test_raise_if_no_platform(hass: HomeAssistant) -> None:
"""Test we raise if 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"
# 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
# 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 raise
error = re.escape(
"Detected code that Entity hello.world (<class 'homeassistant.helpers.entity.Entity'>)"
" does not have a platform,this may be caused by adding it manually instead"
" of with an EntityComponent helper. Please report this issue"
)
with pytest.raises(
RuntimeError,
match=error,
):
ent.async_write_ha_state()
async def test_invalid_state(
@ -1700,6 +1731,9 @@ async def test_invalid_state(
ent.entity_id = "test.test"
ent.hass = hass
platform = MockEntityPlatform(hass, domain="test", platform_name="test")
await platform.async_add_entities([ent])
ent._attr_state = "x" * 255
ent.async_write_ha_state()
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_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")
await platform.async_add_entities([mock_entity])
@ -1756,20 +1783,27 @@ async def test_suggest_report_issue_custom_component(
mock_entity = CustomComponentEntity()
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")
await platform.async_add_entities([mock_entity])
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"
@ -2629,18 +2663,27 @@ async def test_async_write_ha_state_thread_safety(hass: HomeAssistant) -> None:
ent = entity.Entity()
ent.entity_id = "test.any"
ent.hass = hass
ent.async_write_ha_state()
assert hass.states.get(ent.entity_id)
ent2 = entity.Entity()
ent2.entity_id = "test.any2"
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(
RuntimeError,
match="Detected code that calls async_write_ha_state from a thread.",
):
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(