Change Entity.name default to UNDEFINED (#94574)

* Change Entity.name default to UNDEFINED

* Update typing

* Update Pylint plugin

* Update TTS test
This commit is contained in:
Erik Montnemery 2023-06-15 11:09:53 +02:00 committed by GitHub
parent d369d679c7
commit 334dacc322
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 277 additions and 67 deletions

View File

@ -1,7 +1,7 @@
"""Support for Adax wifi-enabled home heaters."""
from __future__ import annotations
from typing import Any
from typing import Any, cast
from adax import Adax
from adax_local import Adax as AdaxLocal
@ -79,7 +79,10 @@ class AdaxDevice(ClimateEntity):
self._attr_unique_id = f"{heater_data['homeId']}_{heater_data['id']}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, heater_data["id"])},
name=self.name,
# Instead of setting the device name to the entity name, adax
# should be updated to set has_entity_name = True, and set the entity
# name to None
name=cast(str | None, self.name),
manufacturer="Adax",
)

View File

@ -3,6 +3,8 @@ from __future__ import annotations
import logging
from homeassistant.helpers.typing import UndefinedType
from .const import DOMAIN
@ -14,7 +16,7 @@ def service_signal(service: str, *args: str) -> str:
def log_update_error(
logger: logging.Logger,
action: str,
name: str | None,
name: str | UndefinedType | None,
entity_type: str,
error: Exception,
level: int = logging.ERROR,

View File

@ -330,7 +330,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
trace_config: ConfigType,
) -> None:
"""Initialize an automation entity."""
self._attr_name = name
self._name = name
self._trigger_config = trigger_config
self._async_detach_triggers: CALLBACK_TYPE | None = None
self._cond_func = cond_func
@ -348,6 +348,11 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
self._trace_config = trace_config
self._attr_unique_id = automation_id
@property
def name(self) -> str:
"""Return the name of the entity."""
return self._name
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the entity state attributes."""

View File

@ -115,10 +115,15 @@ class CupsSensor(SensorEntity):
def __init__(self, data: CupsData, printer_name: str) -> None:
"""Initialize the CUPS sensor."""
self.data = data
self._attr_name = printer_name
self._name = printer_name
self._printer: dict[str, Any] | None = None
self._attr_available = False
@property
def name(self) -> str:
"""Return the name of the entity."""
return self._name
@property
def native_value(self):
"""Return the state of the sensor."""
@ -149,7 +154,6 @@ class CupsSensor(SensorEntity):
def update(self) -> None:
"""Get the latest data and updates the states."""
self.data.update()
assert self.name is not None
assert self.data.printers is not None
self._printer = self.data.printers.get(self.name)
self._attr_available = self.data.available

View File

@ -1,6 +1,8 @@
"""Base DirecTV Entity."""
from __future__ import annotations
from typing import cast
from directv import DIRECTV
from homeassistant.helpers.entity import DeviceInfo, Entity
@ -24,7 +26,10 @@ class DIRECTVEntity(Entity):
return DeviceInfo(
identifiers={(DOMAIN, self._device_id)},
manufacturer=self.dtv.device.info.brand,
name=self.name,
# Instead of setting the device name to the entity name, directv
# should be updated to set has_entity_name = True, and set the entity
# name to None
name=cast(str | None, self.name),
sw_version=self.dtv.device.info.version,
via_device=(DOMAIN, self.dtv.device.info.receiver_id),
)

View File

@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable, Coroutine
from datetime import datetime
from functools import wraps
import logging
from typing import Any, Concatenate, ParamSpec, TypeVar
from typing import Any, Concatenate, ParamSpec, TypeVar, cast
import httpx
from iaqualink.client import AqualinkClient
@ -243,6 +243,8 @@ class AqualinkEntity(Entity):
identifiers={(DOMAIN, self.unique_id)},
manufacturer=self.dev.manufacturer,
model=self.dev.model,
name=self.name,
# Instead of setting the device name to the entity name, iaqualink
# should be updated to set has_entity_name = True
name=cast(str | None, self.name),
via_device=(DOMAIN, self.dev.system.serial),
)

View File

@ -3,7 +3,7 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, cast
from homeassistant.core import callback
from homeassistant.helpers.entity import DeviceInfo, Entity
@ -28,7 +28,9 @@ class KaleidescapeEntity(Entity):
self._attr_name = f"{KALEIDESCAPE_NAME} {device.system.friendly_name}"
self._attr_device_info = DeviceInfo(
identifiers={(KALEIDESCAPE_DOMAIN, self._device.serial_number)},
name=self.name,
# Instead of setting the device name to the entity name, kaleidescape
# should be updated to set has_entity_name = True
name=cast(str | None, self.name),
model=self._device.system.type,
manufacturer=KALEIDESCAPE_NAME,
sw_version=f"{self._device.system.kos_version}",

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from functools import wraps
import logging
from typing import Any, Concatenate, ParamSpec, TypeVar
from typing import Any, Concatenate, ParamSpec, TypeVar, cast
import plexapi.exceptions
import requests.exceptions
@ -535,7 +535,10 @@ class PlexMediaPlayer(MediaPlayerEntity):
identifiers={(DOMAIN, self.machine_identifier)},
manufacturer=self.device_platform or "Plex",
model=self.device_product or self.device_make,
name=self.name,
# Instead of setting the device name to the entity name, plex
# should be updated to set has_entity_name = True, and set the entity
# name to None
name=cast(str | None, self.name),
sw_version=self.device_version,
via_device=(DOMAIN, self.plex_server.machine_identifier),
)

View File

@ -2,7 +2,7 @@
from __future__ import annotations
import logging
from typing import Any
from typing import Any, cast
from roonapi import split_media_path
import voluptuous as vol
@ -159,7 +159,10 @@ class RoonDevice(MediaPlayerEntity):
dev_model = self.player_data["source_controls"][0].get("display_name")
return DeviceInfo(
identifiers={(DOMAIN, self.unique_id)},
name=self.name,
# Instead of setting the device name to the entity name, roon
# should be updated to set has_entity_name = True, and set the entity
# name to None
name=cast(str | None, self.name),
manufacturer="RoonLabs",
model=dev_model,
via_device=(DOMAIN, self._server.roon_id),

View File

@ -1,6 +1,8 @@
"""Base SamsungTV Entity."""
from __future__ import annotations
from typing import cast
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC, CONF_MODEL, CONF_NAME
from homeassistant.helpers import device_registry as dr
@ -20,7 +22,9 @@ class SamsungTVEntity(Entity):
self._attr_name = config_entry.data.get(CONF_NAME)
self._attr_unique_id = config_entry.unique_id
self._attr_device_info = DeviceInfo(
name=self.name,
# Instead of setting the device name to the entity name, samsungtv
# should be updated to set has_entity_name = True
name=cast(str | None, self.name),
manufacturer=config_entry.data.get(CONF_MANUFACTURER),
model=config_entry.data.get(CONF_MODEL),
)

View File

@ -44,7 +44,7 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.network import get_url
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.typing import UNDEFINED, ConfigType
from homeassistant.util import dt as dt_util, language as language_util
from .const import (
@ -610,7 +610,7 @@ class SpeechManager:
async def get_tts_data() -> str:
"""Handle data available."""
if engine_instance.name is None:
if engine_instance.name is None or engine_instance.name is UNDEFINED:
raise HomeAssistantError("TTS engine name is not set.")
if isinstance(engine_instance, Provider):

View File

@ -15,6 +15,7 @@ from homeassistant.const import EntityCategory, Platform, UnitOfMass, UnitOfTemp
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import UndefinedType
from .core import discovery
from .core.const import (
@ -334,7 +335,7 @@ class ZhaNumber(ZhaEntity, NumberEntity):
return super().native_step
@property
def name(self) -> str | None:
def name(self) -> str | UndefinedType | None:
"""Return the name of the number entity."""
description = self._analog_output_cluster_handler.description
if description is not None and len(description) > 0:

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 is implicitly using device name
_implicit_device_name_reported = False
# If we reported this entity was added without its platform set
_no_platform_reported = False
@ -319,6 +322,53 @@ class Entity(ABC):
"""Return a unique ID."""
return self._attr_unique_id
@property
def use_device_name(self) -> bool:
"""Return if this entity does not have its own name.
Should be True if the entity represents the single main feature of a device.
"""
def report_implicit_device_name() -> None:
"""Report entities which use implicit device name."""
if self._implicit_device_name_reported:
return
report_issue = self._suggest_report_issue()
_LOGGER.warning(
(
"Entity %s (%s) is implicitly using device name by not setting its "
"name. Instead, the name should be set to None, please %s"
),
self.entity_id,
type(self),
report_issue,
)
self._implicit_device_name_reported = True
if hasattr(self, "_attr_name"):
return not self._attr_name
if name_translation_key := self._name_translation_key():
if name_translation_key in self.platform.platform_translations:
return False
if hasattr(self, "entity_description"):
if not (name := self.entity_description.name):
return True
if name is UNDEFINED:
# Backwards compatibility with leaving EntityDescription.name unassigned
# for device name.
# Deprecated in HA Core 2023.6, remove in HA Core 2023.9
report_implicit_device_name()
return True
return False
if self.name is UNDEFINED:
# Backwards compatibility with not overriding name property for device name.
# Deprecated in HA Core 2023.6, remove in HA Core 2023.9
report_implicit_device_name()
return True
return not self.name
@property
def has_entity_name(self) -> bool:
"""Return if the name of the entity is describing only the entity itself."""
@ -344,16 +394,23 @@ class Entity(ABC):
"""Return True if an unnamed entity should be named by its device class."""
return False
def _name_translation_key(self) -> str | None:
"""Return translation key for entity name."""
if self.translation_key is None:
return None
return (
f"component.{self.platform.platform_name}.entity.{self.platform.domain}"
f".{self.translation_key}.name"
)
@property
def name(self) -> str | None:
def name(self) -> str | UndefinedType | None:
"""Return the name of the entity."""
if hasattr(self, "_attr_name"):
return self._attr_name
if self.translation_key is not None and self.has_entity_name:
name_translation_key = (
f"component.{self.platform.platform_name}.entity.{self.platform.domain}"
f".{self.translation_key}.name"
)
if self.has_entity_name and (
name_translation_key := self._name_translation_key()
):
if name_translation_key in self.platform.platform_translations:
name: str = self.platform.platform_translations[name_translation_key]
return name
@ -361,15 +418,13 @@ class Entity(ABC):
description_name = self.entity_description.name
if description_name is UNDEFINED and self._default_to_device_class_name():
return self._device_class_name()
if description_name is not UNDEFINED:
return description_name
return None
return description_name
# The entity has no name set by _attr_name, translation_key or entity_description
# Check if the entity should be named by its device class
if self._default_to_device_class_name():
return self._device_class_name()
return None
return UNDEFINED
@property
def state(self) -> StateType:
@ -653,16 +708,20 @@ class Entity(ABC):
If has_entity_name is False, this returns self.name
If has_entity_name is True, this returns device.name + self.name
"""
name = self.name
if name is UNDEFINED:
name = None
if not self.has_entity_name or not self.registry_entry:
return self.name
return name
device_registry = dr.async_get(self.hass)
if not (device_id := self.registry_entry.device_id) or not (
device_entry := device_registry.async_get(device_id)
):
return self.name
return name
if not (name := self.name):
if self.use_device_name:
return device_entry.name_by_user or device_entry.name
return f"{device_entry.name_by_user or device_entry.name} {name}"

View File

@ -45,7 +45,7 @@ from .device_registry import DeviceRegistry
from .entity_registry import EntityRegistry, RegistryEntryDisabler, RegistryEntryHider
from .event import async_call_later, async_track_time_interval
from .issue_registry import IssueSeverity, async_create_issue
from .typing import ConfigType, DiscoveryInfoType
from .typing import UNDEFINED, ConfigType, DiscoveryInfoType
if TYPE_CHECKING:
from .entity import Entity
@ -552,6 +552,10 @@ class EntityPlatform:
suggested_object_id: str | None = None
generate_new_entity_id = False
entity_name = entity.name
if entity_name is UNDEFINED:
entity_name = None
# Get entity_id from unique ID registration
if entity.unique_id is not None:
registered_entity_id = entity_registry.async_get_entity_id(
@ -645,12 +649,12 @@ class EntityPlatform:
else:
if device and entity.has_entity_name:
device_name = device.name_by_user or device.name
if not entity.name:
if entity.use_device_name:
suggested_object_id = device_name
else:
suggested_object_id = f"{device_name} {entity.name}"
suggested_object_id = f"{device_name} {entity_name}"
if not suggested_object_id:
suggested_object_id = entity.name
suggested_object_id = entity_name
if self.entity_namespace is not None:
suggested_object_id = f"{self.entity_namespace} {suggested_object_id}"
@ -678,7 +682,7 @@ class EntityPlatform:
known_object_ids=self.entities.keys(),
original_device_class=entity.device_class,
original_icon=entity.icon,
original_name=entity.name,
original_name=entity_name,
suggested_object_id=suggested_object_id,
supported_features=entity.supported_features,
translation_key=entity.translation_key,
@ -705,7 +709,7 @@ class EntityPlatform:
# Generate entity ID
if entity.entity_id is None or generate_new_entity_id:
suggested_object_id = (
suggested_object_id or entity.name or DEVICE_DEFAULT_NAME
suggested_object_id or entity_name or DEVICE_DEFAULT_NAME
)
if self.entity_namespace is not None:
@ -732,7 +736,7 @@ class EntityPlatform:
self.logger.debug(
"Not adding entity %s because it's disabled",
entry.name
or entity.name
or entity_name
or f'"{self.platform_name} {entity.unique_id}"',
)
entity.add_to_platform_abort()

View File

@ -573,7 +573,7 @@ _ENTITY_MATCH: list[TypeHintMatch] = [
),
TypeHintMatch(
function_name="name",
return_type=["str", None],
return_type=["str", "UndefinedType", None],
),
TypeHintMatch(
function_name="state",

View File

@ -22,6 +22,7 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import UNDEFINED
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from homeassistant.util.network import normalize_url
@ -68,7 +69,7 @@ async def test_default_entity_attributes() -> None:
entity = DefaultEntity()
assert entity.hass is None
assert entity.name is None
assert entity.name is UNDEFINED
assert entity.default_language == DEFAULT_LANG
assert entity.supported_languages == SUPPORT_LANGUAGES
assert entity.supported_options is None

View File

@ -17,6 +17,7 @@ from homeassistant.const import (
)
from homeassistant.core import Context, HomeAssistant, HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity, entity_registry as er
from homeassistant.helpers.typing import UNDEFINED
from tests.common import (
MockConfigEntry,
@ -948,39 +949,24 @@ async def test_entity_description_fallback() -> None:
assert getattr(ent, field.name) == getattr(ent_with_description, field.name)
@pytest.mark.parametrize(
("has_entity_name", "entity_name", "expected_friendly_name"),
(
(False, "Entity Blu", "Entity Blu"),
(False, None, None),
(True, "Entity Blu", "Device Bla Entity Blu"),
(True, None, "Device Bla"),
),
)
async def test_friendly_name(
async def _test_friendly_name(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
ent: entity.Entity,
has_entity_name: bool,
entity_name: str | None,
expected_friendly_name: str | None,
warn_implicit_name: bool,
) -> None:
"""Test entity_id is influenced by entity name."""
"""Test friendly name."""
expected_warning = (
f"Entity {ent.entity_id} ({type(ent)}) is implicitly using device name"
)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Mock setup entry method."""
async_add_entities(
[
MockEntity(
unique_id="qwer",
device_info={
"identifiers": {("hue", "1234")},
"connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")},
"name": "Device Bla",
},
has_entity_name=has_entity_name,
name=entity_name,
),
]
)
async_add_entities([ent])
return True
platform = MockPlatform(async_setup_entry=async_setup_entry)
@ -995,6 +981,132 @@ async def test_friendly_name(
assert len(hass.states.async_entity_ids()) == 1
state = hass.states.async_all()[0]
assert state.attributes.get(ATTR_FRIENDLY_NAME) == expected_friendly_name
assert (expected_warning in caplog.text) is warn_implicit_name
@pytest.mark.parametrize(
("has_entity_name", "entity_name", "expected_friendly_name", "warn_implicit_name"),
(
(False, "Entity Blu", "Entity Blu", False),
(False, None, None, False),
(True, "Entity Blu", "Device Bla Entity Blu", False),
(True, None, "Device Bla", False),
),
)
async def test_friendly_name_attr(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
has_entity_name: bool,
entity_name: str | None,
expected_friendly_name: str | None,
warn_implicit_name: bool,
) -> None:
"""Test friendly name when the entity uses _attr_*."""
ent = MockEntity(
unique_id="qwer",
device_info={
"identifiers": {("hue", "1234")},
"connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")},
"name": "Device Bla",
},
)
ent._attr_has_entity_name = has_entity_name
ent._attr_name = entity_name
await _test_friendly_name(
hass,
caplog,
ent,
has_entity_name,
entity_name,
expected_friendly_name,
warn_implicit_name,
)
@pytest.mark.parametrize(
("has_entity_name", "entity_name", "expected_friendly_name", "warn_implicit_name"),
(
(False, "Entity Blu", "Entity Blu", False),
(False, None, None, False),
(False, UNDEFINED, None, False),
(True, "Entity Blu", "Device Bla Entity Blu", False),
(True, None, "Device Bla", False),
(True, UNDEFINED, "Device Bla", True),
),
)
async def test_friendly_name_description(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
has_entity_name: bool,
entity_name: str | None,
expected_friendly_name: str | None,
warn_implicit_name: bool,
) -> None:
"""Test friendly name when the entity has an entity description."""
ent = MockEntity(
unique_id="qwer",
device_info={
"identifiers": {("hue", "1234")},
"connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")},
"name": "Device Bla",
},
)
ent.entity_description = entity.EntityDescription(
"test", has_entity_name=has_entity_name, name=entity_name
)
await _test_friendly_name(
hass,
caplog,
ent,
has_entity_name,
entity_name,
expected_friendly_name,
warn_implicit_name,
)
@pytest.mark.parametrize(
("has_entity_name", "entity_name", "expected_friendly_name", "warn_implicit_name"),
(
(False, "Entity Blu", "Entity Blu", False),
(False, None, None, False),
(False, UNDEFINED, None, False),
(True, "Entity Blu", "Device Bla Entity Blu", False),
(True, None, "Device Bla", False),
(True, UNDEFINED, "Device Bla", True),
),
)
async def test_friendly_name_property(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
has_entity_name: bool,
entity_name: str | None,
expected_friendly_name: str | None,
warn_implicit_name: bool,
) -> None:
"""Test friendly name when the entity has overridden the name property."""
ent = MockEntity(
unique_id="qwer",
device_info={
"identifiers": {("hue", "1234")},
"connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")},
"name": "Device Bla",
},
has_entity_name=has_entity_name,
name=entity_name,
)
await _test_friendly_name(
hass,
caplog,
ent,
has_entity_name,
entity_name,
expected_friendly_name,
warn_implicit_name,
)
@pytest.mark.parametrize(
@ -1028,7 +1140,7 @@ async def test_friendly_name_updated(
expected_friendly_name2: str,
expected_friendly_name3: str,
) -> None:
"""Test entity_id is influenced by entity name."""
"""Test friendly name is updated when device or entity registry updates."""
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Mock setup entry method."""