Allow setting an entity's name by its device class (#90767)

Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
Erik Montnemery 2023-05-09 18:55:55 +02:00 committed by GitHub
parent 337b59ba23
commit 67c1051305
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 141 additions and 35 deletions

View file

@ -28,7 +28,7 @@ from homeassistant.const import (
UnitOfTime, UnitOfTime,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DEVICE_CLASS_NAME, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
@ -105,6 +105,13 @@ def sensor_update_to_bluetooth_data_update(
adv: Aranet4Advertisement, adv: Aranet4Advertisement,
) -> PassiveBluetoothDataUpdate: ) -> PassiveBluetoothDataUpdate:
"""Convert a sensor update to a Bluetooth data update.""" """Convert a sensor update to a Bluetooth data update."""
entity_names: dict[PassiveBluetoothEntityKey, str | None] = {}
for key, desc in SENSOR_DESCRIPTIONS.items():
# PassiveBluetoothDataUpdate does not support DEVICE_CLASS_NAME
# the assert satisfies the type checker and will catch attempts
# to use DEVICE_CLASS_NAME in the entity descriptions.
assert desc.name is not DEVICE_CLASS_NAME
entity_names[_device_key_to_bluetooth_entity_key(adv.device, key)] = desc.name
return PassiveBluetoothDataUpdate( return PassiveBluetoothDataUpdate(
devices={adv.device.address: _sensor_device_info_to_hass(adv)}, devices={adv.device.address: _sensor_device_info_to_hass(adv)},
entity_descriptions={ entity_descriptions={
@ -117,10 +124,7 @@ def sensor_update_to_bluetooth_data_update(
) )
for key in SENSOR_DESCRIPTIONS for key in SENSOR_DESCRIPTIONS
}, },
entity_names={ entity_names=entity_names,
_device_key_to_bluetooth_entity_key(adv.device, key): desc.name
for key, desc in SENSOR_DESCRIPTIONS.items()
},
) )

View file

@ -4,7 +4,7 @@ from __future__ import annotations
from pybalboa import EVENT_UPDATE, SpaClient from pybalboa import EVENT_UPDATE, SpaClient
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import DeviceClassName, DeviceInfo, Entity
from .const import DOMAIN from .const import DOMAIN
@ -12,7 +12,9 @@ from .const import DOMAIN
class BalboaBaseEntity(Entity): class BalboaBaseEntity(Entity):
"""Balboa base entity.""" """Balboa base entity."""
def __init__(self, client: SpaClient, name: str | None = None) -> None: def __init__(
self, client: SpaClient, name: str | DeviceClassName | None = None
) -> None:
"""Initialize the control.""" """Initialize the control."""
mac = client.mac_address mac = client.mac_address
model = client.model model = client.model

View file

@ -35,6 +35,11 @@ class BondButtonEntityDescription(
): ):
"""Class to describe a Bond Button entity.""" """Class to describe a Bond Button entity."""
# BondEntity does not support DEVICE_CLASS_NAME
# Restrict the type to satisfy the type checker and catch attempts
# to use DEVICE_CLASS_NAME in the entity descriptions.
name: str | None = None
STOP_BUTTON = BondButtonEntityDescription( STOP_BUTTON = BondButtonEntityDescription(
key=Action.STOP, key=Action.STOP,

View file

@ -88,7 +88,7 @@ class BruntDevice(
self._attr_attribution = ATTRIBUTION self._attr_attribution = ATTRIBUTION
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)}, # type: ignore[arg-type] identifiers={(DOMAIN, self._attr_unique_id)}, # type: ignore[arg-type]
name=self._attr_name, name=self._thing.name,
via_device=(DOMAIN, self._entry_id), via_device=(DOMAIN, self._entry_id),
manufacturer="Brunt", manufacturer="Brunt",
sw_version=self._thing.fw_version, sw_version=self._thing.fw_version,

View file

@ -30,8 +30,8 @@ class FreeboxHomeEntity(Entity):
self._node = node self._node = node
self._sub_node = sub_node self._sub_node = sub_node
self._id = node["id"] self._id = node["id"]
self._attr_name = node["label"].strip() self._device_name = node["label"].strip()
self._device_name = self._attr_name self._attr_name = self._device_name
self._attr_unique_id = f"{self._router.mac}-node_{self._id}" self._attr_unique_id = f"{self._router.mac}-node_{self._id}"
if sub_node is not None: if sub_node is not None:

View file

@ -28,6 +28,10 @@ class IncomfortSensorEntityDescription(SensorEntityDescription):
"""Describes Incomfort sensor entity.""" """Describes Incomfort sensor entity."""
extra_key: str | None = None extra_key: str | None = None
# IncomfortSensor does not support DEVICE_CLASS_NAME
# Restrict the type to satisfy the type checker and catch attempts
# to use DEVICE_CLASS_NAME in the entity descriptions.
name: str | None = None
SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = ( SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = (

View file

@ -14,6 +14,7 @@ from homeassistant.const import (
STATE_ON, STATE_ON,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DEVICE_CLASS_NAME
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@ -73,6 +74,11 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity):
# this ensures that idx = bit position of value in result # this ensures that idx = bit position of value in result
# polling is done with the base class # polling is done with the base class
name = self._attr_name if self._attr_name else "modbus_sensor" name = self._attr_name if self._attr_name else "modbus_sensor"
# DataUpdateCoordinator does not support DEVICE_CLASS_NAME
# the assert satisfies the type checker and will catch attempts
# to use DEVICE_CLASS_NAME in _attr_name.
assert name is not DEVICE_CLASS_NAME
self._coordinator = DataUpdateCoordinator( self._coordinator = DataUpdateCoordinator(
hass, hass,
_LOGGER, _LOGGER,

View file

@ -17,6 +17,7 @@ from homeassistant.const import (
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DEVICE_CLASS_NAME
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
@ -79,6 +80,11 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity):
# this ensures that idx = bit position of value in result # this ensures that idx = bit position of value in result
# polling is done with the base class # polling is done with the base class
name = self._attr_name if self._attr_name else "modbus_sensor" name = self._attr_name if self._attr_name else "modbus_sensor"
# DataUpdateCoordinator does not support DEVICE_CLASS_NAME
# the assert satisfies the type checker and will catch attempts
# to use DEVICE_CLASS_NAME in _attr_name.
assert name is not DEVICE_CLASS_NAME
self._coordinator = DataUpdateCoordinator( self._coordinator = DataUpdateCoordinator(
hass, hass,
_LOGGER, _LOGGER,

View file

@ -67,7 +67,7 @@ class MinutPointAlarmControl(AlarmControlPanelEntity):
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(POINT_DOMAIN, home_id)}, identifiers={(POINT_DOMAIN, home_id)},
manufacturer="Minut", manufacturer="Minut",
name=self._attr_name, name=self._home["name"],
) )
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:

View file

@ -19,6 +19,7 @@ from homeassistant.helpers.device_registry import (
async_get as dr_async_get, async_get as dr_async_get,
format_mac, format_mac,
) )
from homeassistant.helpers.entity import DEVICE_CLASS_NAME, DeviceClassName
from homeassistant.helpers.entity_registry import async_get as er_async_get from homeassistant.helpers.entity_registry import async_get as er_async_get
from homeassistant.helpers.typing import EventType from homeassistant.helpers.typing import EventType
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
@ -72,12 +73,16 @@ def get_number_of_channels(device: BlockDevice, block: Block) -> int:
def get_block_entity_name( def get_block_entity_name(
device: BlockDevice, device: BlockDevice,
block: Block | None, block: Block | None,
description: str | None = None, description: str | DeviceClassName | None = None,
) -> str: ) -> str:
"""Naming for block based switch and sensors.""" """Naming for block based switch and sensors."""
channel_name = get_block_channel_name(device, block) channel_name = get_block_channel_name(device, block)
if description: if description:
# It's not possible to do string manipulations on DEVICE_CLASS_NAME
# the assert satisfies the type checker and will catch attempts
# to use DEVICE_CLASS_NAME as description.
assert description is not DEVICE_CLASS_NAME
return f"{channel_name} {description.lower()}" return f"{channel_name} {description.lower()}"
return channel_name return channel_name
@ -301,12 +306,16 @@ def get_rpc_channel_name(device: RpcDevice, key: str) -> str:
def get_rpc_entity_name( def get_rpc_entity_name(
device: RpcDevice, key: str, description: str | None = None device: RpcDevice, key: str, description: str | DeviceClassName | None = None
) -> str: ) -> str:
"""Naming for RPC based switch and sensors.""" """Naming for RPC based switch and sensors."""
channel_name = get_rpc_channel_name(device, key) channel_name = get_rpc_channel_name(device, key)
if description: if description:
# It's not possible to do string manipulations on DEVICE_CLASS_NAME
# the assert satisfies the type checker and will catch attempts
# to use DEVICE_CLASS_NAME as description.
assert description is not DEVICE_CLASS_NAME
return f"{channel_name} {description.lower()}" return f"{channel_name} {description.lower()}"
return channel_name return channel_name

View file

@ -30,7 +30,7 @@ from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DEVICE_CLASS_NAME, DeviceClassName, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MODULES from .const import DOMAIN, MODULES
@ -279,13 +279,17 @@ class SystemBridgeEntity(CoordinatorEntity[SystemBridgeDataUpdateCoordinator]):
coordinator: SystemBridgeDataUpdateCoordinator, coordinator: SystemBridgeDataUpdateCoordinator,
api_port: int, api_port: int,
key: str, key: str,
name: str | None, name: str | DeviceClassName | None,
) -> None: ) -> None:
"""Initialize the System Bridge entity.""" """Initialize the System Bridge entity."""
super().__init__(coordinator) super().__init__(coordinator)
self._hostname = coordinator.data.system.hostname self._hostname = coordinator.data.system.hostname
self._key = f"{self._hostname}_{key}" self._key = f"{self._hostname}_{key}"
# It's not possible to do string manipulations on DEVICE_CLASS_NAME
# the assert satisfies the type checker and will catch attempts
# to use DEVICE_CLASS_NAME as name.
assert name is not DEVICE_CLASS_NAME
self._name = f"{self._hostname} {name}" self._name = f"{self._hostname} {name}"
self._configuration_url = ( self._configuration_url = (
f"http://{self._hostname}:{api_port}/app/settings.html" f"http://{self._hostname}:{api_port}/app/settings.html"

View file

@ -32,6 +32,7 @@ from homeassistant.const import (
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DEVICE_CLASS_NAME
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.util.unit_conversion import DistanceConverter, SpeedConverter from homeassistant.util.unit_conversion import DistanceConverter, SpeedConverter
@ -349,6 +350,10 @@ class BaseTomorrowioSensorEntity(TomorrowioEntity, SensorEntity):
"""Initialize Tomorrow.io Sensor Entity.""" """Initialize Tomorrow.io Sensor Entity."""
super().__init__(config_entry, coordinator, api_version) super().__init__(config_entry, coordinator, api_version)
self.entity_description = description self.entity_description = description
# It's not possible to do string manipulations on DEVICE_CLASS_NAME
# the assert satisfies the type checker and will catch attempts
# to use DEVICE_CLASS_NAME in the entity descriptions.
assert description.name is not DEVICE_CLASS_NAME
self._attr_name = f"{self._config_entry.data[CONF_NAME]} - {description.name}" self._attr_name = f"{self._config_entry.data[CONF_NAME]} - {description.name}"
self._attr_unique_id = ( self._attr_unique_id = (
f"{self._config_entry.unique_id}_{slugify(description.name)}" f"{self._config_entry.unique_id}_{slugify(description.name)}"

View file

@ -22,7 +22,12 @@ from pyunifiprotect.data import (
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.helpers.device_registry as dr import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.entity import (
DEVICE_CLASS_NAME,
DeviceInfo,
Entity,
EntityDescription,
)
from .const import ( from .const import (
ATTR_EVENT_ID, ATTR_EVENT_ID,
@ -199,6 +204,10 @@ class ProtectDeviceEntity(Entity):
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{self.device.mac}_{description.key}" self._attr_unique_id = f"{self.device.mac}_{description.key}"
name = description.name or "" name = description.name or ""
# It's not possible to do string manipulations on DEVICE_CLASS_NAME
# the assert satisfies the type checker and will catch attempts
# to use DEVICE_CLASS_NAME in the entity descriptions.
assert name is not DEVICE_CLASS_NAME
self._attr_name = f"{self.device.display_name} {name.title()}" self._attr_name = f"{self.device.display_name} {name.title()}"
self._attr_attribution = DEFAULT_ATTRIBUTION self._attr_attribution = DEFAULT_ATTRIBUTION

View file

@ -131,6 +131,8 @@ async def async_setup_entry(
class VizioDevice(MediaPlayerEntity): class VizioDevice(MediaPlayerEntity):
"""Media Player implementation which performs REST requests to device.""" """Media Player implementation which performs REST requests to device."""
_attr_name: str
def __init__( def __init__(
self, self,
config_entry: ConfigEntry, config_entry: ConfigEntry,

View file

@ -28,6 +28,10 @@ from .wemo_device import DeviceCoordinator
class AttributeSensorDescription(SensorEntityDescription): class AttributeSensorDescription(SensorEntityDescription):
"""SensorEntityDescription for WeMo AttributeSensor entities.""" """SensorEntityDescription for WeMo AttributeSensor entities."""
# AttributeSensor does not support DEVICE_CLASS_NAME
# the assert satisfies the type checker and will catch attempts
# to use DEVICE_CLASS_NAME in the entity descriptions.
name: str | None = None
state_conversion: Callable[[StateType], StateType] | None = None state_conversion: Callable[[StateType], StateType] | None = None
unique_id_suffix: str | None = None unique_id_suffix: str | None = None

View file

@ -8,7 +8,7 @@ from zwave_js_server.model.value import Value as ZwaveValue, get_value_id_str
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import DEVICE_CLASS_NAME, DeviceInfo, Entity
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
from .discovery import ZwaveDiscoveryInfo from .discovery import ZwaveDiscoveryInfo
@ -136,6 +136,10 @@ class ZWaveBaseEntity(Entity):
and self.entity_description and self.entity_description
and self.entity_description.name and self.entity_description.name
): ):
# It's not possible to do string manipulations on DEVICE_CLASS_NAME
# the assert satisfies the type checker and will catch attempts
# to use DEVICE_CLASS_NAME in the entity descriptions.
assert self.entity_description.name is not DEVICE_CLASS_NAME
name = self.entity_description.name name = self.entity_description.name
if name_prefix: if name_prefix:

View file

@ -115,7 +115,7 @@ async def async_setup_platforms(
class ZWaveMeEntity(Entity): class ZWaveMeEntity(Entity):
"""Representation of a ZWaveMe device.""" """Representation of a ZWaveMe device."""
def __init__(self, controller, device): def __init__(self, controller: ZWaveMeController, device: ZWaveMeData) -> None:
"""Initialize the device.""" """Initialize the device."""
self.controller = controller self.controller = controller
self.device = device self.device = device
@ -124,13 +124,9 @@ class ZWaveMeEntity(Entity):
f"{self.controller.config.unique_id}-{self.device.id}" f"{self.controller.config.unique_id}-{self.device.id}"
) )
self._attr_should_poll = False self._attr_should_poll = False
self._attr_device_info = DeviceInfo(
@property
def device_info(self) -> DeviceInfo:
"""Return device specific attributes."""
return DeviceInfo(
identifiers={(DOMAIN, self.device.deviceIdentifier)}, identifiers={(DOMAIN, self.device.deviceIdentifier)},
name=self._attr_name, name=device.title,
manufacturer=self.device.manufacturer, manufacturer=self.device.manufacturer,
sw_version=self.device.firmware, sw_version=self.device.firmware,
suggested_area=self.device.locationName, suggested_area=self.device.locationName,

View file

@ -52,6 +52,16 @@ DATA_ENTITY_SOURCE = "entity_info"
SOURCE_CONFIG_ENTRY = "config_entry" SOURCE_CONFIG_ENTRY = "config_entry"
SOURCE_PLATFORM_CONFIG = "platform_config" SOURCE_PLATFORM_CONFIG = "platform_config"
class DeviceClassName(Enum):
"""Singleton to use device class name."""
_singleton = 0
DEVICE_CLASS_NAME = DeviceClassName._singleton # pylint: disable=protected-access
# Used when converting float states to string: limit precision according to machine # Used when converting float states to string: limit precision according to machine
# epsilon to make the string representation readable # epsilon to make the string representation readable
FLOAT_PRECISION = abs(int(math.floor(math.log10(abs(sys.float_info.epsilon))))) - 1 FLOAT_PRECISION = abs(int(math.floor(math.log10(abs(sys.float_info.epsilon))))) - 1
@ -219,7 +229,7 @@ class EntityDescription:
force_update: bool = False force_update: bool = False
icon: str | None = None icon: str | None = None
has_entity_name: bool = False has_entity_name: bool = False
name: str | None = None name: str | DeviceClassName | None = None
translation_key: str | None = None translation_key: str | None = None
unit_of_measurement: str | None = None unit_of_measurement: str | None = None
@ -288,7 +298,7 @@ class Entity(ABC):
_attr_extra_state_attributes: MutableMapping[str, Any] _attr_extra_state_attributes: MutableMapping[str, Any]
_attr_force_update: bool _attr_force_update: bool
_attr_icon: str | None _attr_icon: str | None
_attr_name: str | None _attr_name: str | DeviceClassName | None
_attr_should_poll: bool = True _attr_should_poll: bool = True
_attr_state: StateType = STATE_UNKNOWN _attr_state: StateType = STATE_UNKNOWN
_attr_supported_features: int | None = None _attr_supported_features: int | None = None
@ -318,10 +328,24 @@ class Entity(ABC):
return self.entity_description.has_entity_name return self.entity_description.has_entity_name
return False return False
def _device_class_name(self) -> str | None:
"""Return a translated name of the entity based on its device class."""
assert self.platform
if not self.has_entity_name:
return None
device_class_key = self.device_class or "_"
name_translation_key = (
f"component.{self.platform.domain}.entity_component."
f"{device_class_key}.name"
)
return self.platform.component_translations.get(name_translation_key)
@property @property
def name(self) -> str | None: def name(self) -> str | None:
"""Return the name of the entity.""" """Return the name of the entity."""
if hasattr(self, "_attr_name"): if hasattr(self, "_attr_name"):
if self._attr_name is DEVICE_CLASS_NAME:
return self._device_class_name()
return self._attr_name return self._attr_name
if self.translation_key is not None and self.has_entity_name: if self.translation_key is not None and self.has_entity_name:
assert self.platform assert self.platform
@ -329,10 +353,12 @@ class Entity(ABC):
f"component.{self.platform.platform_name}.entity.{self.platform.domain}" f"component.{self.platform.platform_name}.entity.{self.platform.domain}"
f".{self.translation_key}.name" f".{self.translation_key}.name"
) )
if name_translation_key in self.platform.entity_translations: if name_translation_key in self.platform.platform_translations:
name: str = self.platform.entity_translations[name_translation_key] name: str = self.platform.platform_translations[name_translation_key]
return name return name
if hasattr(self, "entity_description"): if hasattr(self, "entity_description"):
if self.entity_description.name is DEVICE_CLASS_NAME:
return self._device_class_name()
return self.entity_description.name return self.entity_description.name
return None return None

View file

@ -125,7 +125,8 @@ class EntityPlatform:
self.entity_namespace = entity_namespace self.entity_namespace = entity_namespace
self.config_entry: config_entries.ConfigEntry | None = None self.config_entry: config_entries.ConfigEntry | None = None
self.entities: dict[str, Entity] = {} self.entities: dict[str, Entity] = {}
self.entity_translations: dict[str, Any] = {} self.component_translations: dict[str, Any] = {}
self.platform_translations: dict[str, Any] = {}
self._tasks: list[asyncio.Task[None]] = [] self._tasks: list[asyncio.Task[None]] = []
# Stop tracking tasks after setup is completed # Stop tracking tasks after setup is completed
self._setup_complete = False self._setup_complete = False
@ -279,7 +280,15 @@ class EntityPlatform:
full_name = f"{self.domain}.{self.platform_name}" full_name = f"{self.domain}.{self.platform_name}"
try: try:
self.entity_translations = await translation.async_get_translations( self.component_translations = await translation.async_get_translations(
hass, hass.config.language, "entity_component", {self.domain}
)
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.debug(
"Could not load translations for %s", self.domain, exc_info=err
)
try:
self.platform_translations = await translation.async_get_translations(
hass, hass.config.language, "entity", {self.platform_name} hass, hass.config.language, "entity", {self.platform_name}
) )
except Exception as err: # pylint: disable=broad-exception-caught except Exception as err: # pylint: disable=broad-exception-caught

View file

@ -172,6 +172,8 @@ class _TemplateAttribute:
class TemplateEntity(Entity): class TemplateEntity(Entity):
"""Entity that uses templates to calculate attributes.""" """Entity that uses templates to calculate attributes."""
_attr_name: str | None
_attr_available = True _attr_available = True
_attr_entity_picture = None _attr_entity_picture = None
_attr_icon = None _attr_icon = None

View file

@ -1,9 +1,10 @@
"""esphome session fixtures.""" """esphome session fixtures."""
from __future__ import annotations from __future__ import annotations
from asyncio import Event
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
from aioesphomeapi import APIClient, APIVersion, DeviceInfo from aioesphomeapi import APIClient, APIVersion, DeviceInfo, ReconnectLogic
import pytest import pytest
from zeroconf import Zeroconf from zeroconf import Zeroconf
@ -158,10 +159,18 @@ async def mock_voice_assistant_v1_entry(
mock_client.device_info = AsyncMock(return_value=device_info) mock_client.device_info = AsyncMock(return_value=device_info)
mock_client.subscribe_voice_assistant = AsyncMock(return_value=Mock()) mock_client.subscribe_voice_assistant = AsyncMock(return_value=Mock())
await hass.config_entries.async_setup(entry.entry_id) try_connect_done = Event()
await hass.async_block_till_done() real_try_connect = ReconnectLogic._try_connect
await hass.async_block_till_done()
await hass.async_block_till_done() async def mock_try_connect(self):
"""Set an event when ReconnectLogic._try_connect has been awaited."""
result = await real_try_connect(self)
try_connect_done.set()
return result
with patch.object(ReconnectLogic, "_try_connect", mock_try_connect):
await hass.config_entries.async_setup(entry.entry_id)
await try_connect_done.wait()
return entry return entry