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.""" """Support for Adax wifi-enabled home heaters."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any, cast
from adax import Adax from adax import Adax
from adax_local import Adax as AdaxLocal 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_unique_id = f"{heater_data['homeId']}_{heater_data['id']}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, heater_data["id"])}, 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", manufacturer="Adax",
) )

View file

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

View file

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

View file

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

View file

@ -1,6 +1,8 @@
"""Base DirecTV Entity.""" """Base DirecTV Entity."""
from __future__ import annotations from __future__ import annotations
from typing import cast
from directv import DIRECTV from directv import DIRECTV
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import DeviceInfo, Entity
@ -24,7 +26,10 @@ class DIRECTVEntity(Entity):
return DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self._device_id)}, identifiers={(DOMAIN, self._device_id)},
manufacturer=self.dtv.device.info.brand, 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, sw_version=self.dtv.device.info.version,
via_device=(DOMAIN, self.dtv.device.info.receiver_id), 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 datetime import datetime
from functools import wraps from functools import wraps
import logging import logging
from typing import Any, Concatenate, ParamSpec, TypeVar from typing import Any, Concatenate, ParamSpec, TypeVar, cast
import httpx import httpx
from iaqualink.client import AqualinkClient from iaqualink.client import AqualinkClient
@ -243,6 +243,8 @@ class AqualinkEntity(Entity):
identifiers={(DOMAIN, self.unique_id)}, identifiers={(DOMAIN, self.unique_id)},
manufacturer=self.dev.manufacturer, manufacturer=self.dev.manufacturer,
model=self.dev.model, 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), via_device=(DOMAIN, self.dev.system.serial),
) )

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, cast
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity import DeviceInfo, Entity 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_name = f"{KALEIDESCAPE_NAME} {device.system.friendly_name}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(KALEIDESCAPE_DOMAIN, self._device.serial_number)}, 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, model=self._device.system.type,
manufacturer=KALEIDESCAPE_NAME, manufacturer=KALEIDESCAPE_NAME,
sw_version=f"{self._device.system.kos_version}", sw_version=f"{self._device.system.kos_version}",

View file

@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from functools import wraps from functools import wraps
import logging import logging
from typing import Any, Concatenate, ParamSpec, TypeVar from typing import Any, Concatenate, ParamSpec, TypeVar, cast
import plexapi.exceptions import plexapi.exceptions
import requests.exceptions import requests.exceptions
@ -535,7 +535,10 @@ class PlexMediaPlayer(MediaPlayerEntity):
identifiers={(DOMAIN, self.machine_identifier)}, identifiers={(DOMAIN, self.machine_identifier)},
manufacturer=self.device_platform or "Plex", manufacturer=self.device_platform or "Plex",
model=self.device_product or self.device_make, 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, sw_version=self.device_version,
via_device=(DOMAIN, self.plex_server.machine_identifier), via_device=(DOMAIN, self.plex_server.machine_identifier),
) )

View file

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any from typing import Any, cast
from roonapi import split_media_path from roonapi import split_media_path
import voluptuous as vol import voluptuous as vol
@ -159,7 +159,10 @@ class RoonDevice(MediaPlayerEntity):
dev_model = self.player_data["source_controls"][0].get("display_name") dev_model = self.player_data["source_controls"][0].get("display_name")
return DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self.unique_id)}, 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", manufacturer="RoonLabs",
model=dev_model, model=dev_model,
via_device=(DOMAIN, self._server.roon_id), via_device=(DOMAIN, self._server.roon_id),

View file

@ -1,6 +1,8 @@
"""Base SamsungTV Entity.""" """Base SamsungTV Entity."""
from __future__ import annotations from __future__ import annotations
from typing import cast
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC, CONF_MODEL, CONF_NAME from homeassistant.const import CONF_MAC, CONF_MODEL, CONF_NAME
from homeassistant.helpers import device_registry as dr 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_name = config_entry.data.get(CONF_NAME)
self._attr_unique_id = config_entry.unique_id self._attr_unique_id = config_entry.unique_id
self._attr_device_info = DeviceInfo( 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), manufacturer=config_entry.data.get(CONF_MANUFACTURER),
model=config_entry.data.get(CONF_MODEL), 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.event import async_call_later
from homeassistant.helpers.network import get_url from homeassistant.helpers.network import get_url
from homeassistant.helpers.restore_state import RestoreEntity 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 homeassistant.util import dt as dt_util, language as language_util
from .const import ( from .const import (
@ -610,7 +610,7 @@ class SpeechManager:
async def get_tts_data() -> str: async def get_tts_data() -> str:
"""Handle data available.""" """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.") raise HomeAssistantError("TTS engine name is not set.")
if isinstance(engine_instance, Provider): 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.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import UndefinedType
from .core import discovery from .core import discovery
from .core.const import ( from .core.const import (
@ -334,7 +335,7 @@ class ZhaNumber(ZhaEntity, NumberEntity):
return super().native_step return super().native_step
@property @property
def name(self) -> str | None: def name(self) -> str | UndefinedType | None:
"""Return the name of the number entity.""" """Return the name of the number entity."""
description = self._analog_output_cluster_handler.description description = self._analog_output_cluster_handler.description
if description is not None and len(description) > 0: 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. # 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 is implicitly using device name
_implicit_device_name_reported = False
# If we reported this entity was added without its platform set # If we reported this entity was added without its platform set
_no_platform_reported = False _no_platform_reported = False
@ -319,6 +322,53 @@ class Entity(ABC):
"""Return a unique ID.""" """Return a unique ID."""
return self._attr_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 @property
def has_entity_name(self) -> bool: def has_entity_name(self) -> bool:
"""Return if the name of the entity is describing only the entity itself.""" """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 True if an unnamed entity should be named by its device class."""
return False 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 @property
def name(self) -> str | None: def name(self) -> str | UndefinedType | None:
"""Return the name of the entity.""" """Return the name of the entity."""
if hasattr(self, "_attr_name"): if hasattr(self, "_attr_name"):
return self._attr_name return self._attr_name
if self.translation_key is not None and self.has_entity_name: if self.has_entity_name and (
name_translation_key = ( name_translation_key := self._name_translation_key()
f"component.{self.platform.platform_name}.entity.{self.platform.domain}" ):
f".{self.translation_key}.name"
)
if name_translation_key in self.platform.platform_translations: if name_translation_key in self.platform.platform_translations:
name: str = self.platform.platform_translations[name_translation_key] name: str = self.platform.platform_translations[name_translation_key]
return name return name
@ -361,15 +418,13 @@ class Entity(ABC):
description_name = self.entity_description.name description_name = self.entity_description.name
if description_name is UNDEFINED and self._default_to_device_class_name(): if description_name is UNDEFINED and self._default_to_device_class_name():
return self._device_class_name() return self._device_class_name()
if description_name is not UNDEFINED: return description_name
return description_name
return None
# The entity has no name set by _attr_name, translation_key or entity_description # 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 # Check if the entity should be named by its device class
if self._default_to_device_class_name(): if self._default_to_device_class_name():
return self._device_class_name() return self._device_class_name()
return None return UNDEFINED
@property @property
def state(self) -> StateType: 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 False, this returns self.name
If has_entity_name is True, this returns device.name + 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: if not self.has_entity_name or not self.registry_entry:
return self.name return name
device_registry = dr.async_get(self.hass) device_registry = dr.async_get(self.hass)
if not (device_id := self.registry_entry.device_id) or not ( if not (device_id := self.registry_entry.device_id) or not (
device_entry := device_registry.async_get(device_id) 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 device_entry.name_by_user or device_entry.name
return f"{device_entry.name_by_user or device_entry.name} {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 .entity_registry import EntityRegistry, RegistryEntryDisabler, RegistryEntryHider
from .event import async_call_later, async_track_time_interval from .event import async_call_later, async_track_time_interval
from .issue_registry import IssueSeverity, async_create_issue from .issue_registry import IssueSeverity, async_create_issue
from .typing import ConfigType, DiscoveryInfoType from .typing import UNDEFINED, ConfigType, DiscoveryInfoType
if TYPE_CHECKING: if TYPE_CHECKING:
from .entity import Entity from .entity import Entity
@ -552,6 +552,10 @@ class EntityPlatform:
suggested_object_id: str | None = None suggested_object_id: str | None = None
generate_new_entity_id = False generate_new_entity_id = False
entity_name = entity.name
if entity_name is UNDEFINED:
entity_name = None
# Get entity_id from unique ID registration # Get entity_id from unique ID registration
if entity.unique_id is not None: if entity.unique_id is not None:
registered_entity_id = entity_registry.async_get_entity_id( registered_entity_id = entity_registry.async_get_entity_id(
@ -645,12 +649,12 @@ class EntityPlatform:
else: else:
if device and entity.has_entity_name: if device and entity.has_entity_name:
device_name = device.name_by_user or device.name device_name = device.name_by_user or device.name
if not entity.name: if entity.use_device_name:
suggested_object_id = device_name suggested_object_id = device_name
else: else:
suggested_object_id = f"{device_name} {entity.name}" suggested_object_id = f"{device_name} {entity_name}"
if not suggested_object_id: if not suggested_object_id:
suggested_object_id = entity.name suggested_object_id = entity_name
if self.entity_namespace is not None: if self.entity_namespace is not None:
suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" suggested_object_id = f"{self.entity_namespace} {suggested_object_id}"
@ -678,7 +682,7 @@ class EntityPlatform:
known_object_ids=self.entities.keys(), known_object_ids=self.entities.keys(),
original_device_class=entity.device_class, original_device_class=entity.device_class,
original_icon=entity.icon, original_icon=entity.icon,
original_name=entity.name, original_name=entity_name,
suggested_object_id=suggested_object_id, suggested_object_id=suggested_object_id,
supported_features=entity.supported_features, supported_features=entity.supported_features,
translation_key=entity.translation_key, translation_key=entity.translation_key,
@ -705,7 +709,7 @@ class EntityPlatform:
# Generate entity ID # Generate entity ID
if entity.entity_id is None or generate_new_entity_id: if entity.entity_id is None or generate_new_entity_id:
suggested_object_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: if self.entity_namespace is not None:
@ -732,7 +736,7 @@ class EntityPlatform:
self.logger.debug( self.logger.debug(
"Not adding entity %s because it's disabled", "Not adding entity %s because it's disabled",
entry.name entry.name
or entity.name or entity_name
or f'"{self.platform_name} {entity.unique_id}"', or f'"{self.platform_name} {entity.unique_id}"',
) )
entity.add_to_platform_abort() entity.add_to_platform_abort()

View file

@ -573,7 +573,7 @@ _ENTITY_MATCH: list[TypeHintMatch] = [
), ),
TypeHintMatch( TypeHintMatch(
function_name="name", function_name="name",
return_type=["str", None], return_type=["str", "UndefinedType", None],
), ),
TypeHintMatch( TypeHintMatch(
function_name="state", 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.const import ATTR_ENTITY_ID, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import UNDEFINED
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.util.network import normalize_url from homeassistant.util.network import normalize_url
@ -68,7 +69,7 @@ async def test_default_entity_attributes() -> None:
entity = DefaultEntity() entity = DefaultEntity()
assert entity.hass is None assert entity.hass is None
assert entity.name is None assert entity.name is UNDEFINED
assert entity.default_language == DEFAULT_LANG assert entity.default_language == DEFAULT_LANG
assert entity.supported_languages == SUPPORT_LANGUAGES assert entity.supported_languages == SUPPORT_LANGUAGES
assert entity.supported_options is None 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.core import Context, HomeAssistant, HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity, entity_registry as er from homeassistant.helpers import device_registry as dr, entity, entity_registry as er
from homeassistant.helpers.typing import UNDEFINED
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
@ -948,39 +949,24 @@ async def test_entity_description_fallback() -> None:
assert getattr(ent, field.name) == getattr(ent_with_description, field.name) assert getattr(ent, field.name) == getattr(ent_with_description, field.name)
@pytest.mark.parametrize( async def _test_friendly_name(
("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(
hass: HomeAssistant, hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
ent: entity.Entity,
has_entity_name: bool, has_entity_name: bool,
entity_name: str | None, entity_name: str | None,
expected_friendly_name: str | None, expected_friendly_name: str | None,
warn_implicit_name: bool,
) -> None: ) -> 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): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Mock setup entry method.""" """Mock setup entry method."""
async_add_entities( async_add_entities([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,
),
]
)
return True return True
platform = MockPlatform(async_setup_entry=async_setup_entry) 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 assert len(hass.states.async_entity_ids()) == 1
state = hass.states.async_all()[0] state = hass.states.async_all()[0]
assert state.attributes.get(ATTR_FRIENDLY_NAME) == expected_friendly_name 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( @pytest.mark.parametrize(
@ -1028,7 +1140,7 @@ async def test_friendly_name_updated(
expected_friendly_name2: str, expected_friendly_name2: str,
expected_friendly_name3: str, expected_friendly_name3: str,
) -> None: ) -> 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): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Mock setup entry method.""" """Mock setup entry method."""