Compare commits

...
Sign in to create a new pull request.

4 commits

Author SHA1 Message Date
Erik
cee882f1e8 Fix bug 2023-05-22 14:46:29 +02:00
Erik
5d934509df Update type annotations 2023-05-12 14:12:15 +02:00
Erik
eea1798a1d Report implicit device name 2023-05-12 13:55:17 +02:00
Erik
f2f76c2b8c Introduce DEVICE_NAME constant for entities which don't have a name 2023-05-12 13:46:28 +02:00
11 changed files with 174 additions and 55 deletions

View file

@ -28,7 +28,7 @@ from homeassistant.const import (
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DEVICE_CLASS_NAME, DeviceInfo
from homeassistant.helpers.entity import DEVICE_CLASS_NAME, DEVICE_NAME, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
@ -107,10 +107,11 @@ def sensor_update_to_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.
# PassiveBluetoothDataUpdate does not support DEVICE_CLASS_NAME or DEVICE_NAME.
# The asserts satisfy the type checker and will catch attempts
# to use DEVICE_CLASS_NAME or DEVICE_NAME in the entity descriptions.
assert desc.name is not DEVICE_CLASS_NAME
assert desc.name is not DEVICE_NAME
entity_names[_device_key_to_bluetooth_entity_key(adv.device, key)] = desc.name
return PassiveBluetoothDataUpdate(
devices={adv.device.address: _sensor_device_info_to_hass(adv)},

View file

@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DEVICE_NAME
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
@ -83,6 +84,9 @@ class BalboaBinarySensorEntity(BalboaEntity, BinarySensorEntity):
self, spa: SpaClient, description: BalboaBinarySensorEntityDescription
) -> None:
"""Initialize a Balboa binary sensor entity."""
# The assert satisfies the type checker and will catch attempts
# to use DEVICE_NAME in the entity descriptions.
assert description.name is not DEVICE_NAME
super().__init__(spa, description.name)
self.entity_description = description

View file

@ -14,7 +14,7 @@ from homeassistant.const import (
STATE_ON,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DEVICE_CLASS_NAME
from homeassistant.helpers.entity import DEVICE_CLASS_NAME, DEVICE_NAME
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@ -75,10 +75,11 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity):
# polling is done with the base class
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.
# DataUpdateCoordinator does not support DEVICE_CLASS_NAME or DEVICE_NAME
# The asserts satisfy the type checker and will catch attempts
# to use DEVICE_CLASS_NAME or DEVICE_NAME in _attr_name.
assert name is not DEVICE_CLASS_NAME
assert name is not DEVICE_NAME
self._coordinator = DataUpdateCoordinator(
hass,
_LOGGER,

View file

@ -17,7 +17,7 @@ from homeassistant.const import (
CONF_UNIT_OF_MEASUREMENT,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DEVICE_CLASS_NAME
from homeassistant.helpers.entity import DEVICE_CLASS_NAME, DEVICE_NAME
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import (
@ -81,10 +81,11 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity):
# polling is done with the base class
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.
# DataUpdateCoordinator does not support DEVICE_CLASS_NAME or DEVICE_NAME
# The asserts satisfy the type checker and will catch attempts
# to use DEVICE_CLASS_NAME or DEVICE_NAME in _attr_name.
assert name is not DEVICE_CLASS_NAME
assert name is not DEVICE_NAME
self._coordinator = DataUpdateCoordinator(
hass,
_LOGGER,

View file

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

View file

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

View file

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

View file

@ -24,6 +24,7 @@ from homeassistant.core import callback
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.entity import (
DEVICE_CLASS_NAME,
DEVICE_NAME,
DeviceInfo,
Entity,
EntityDescription,
@ -204,10 +205,11 @@ class ProtectDeviceEntity(Entity):
self.entity_description = description
self._attr_unique_id = f"{self.device.mac}_{description.key}"
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.
# It's not possible to do string manipulations on DEVICE_CLASS_NAME or
# DEVICE_NAME. The asserts satisfy the type checker and will catch attempts
# to use DEVICE_CLASS_NAME or DEVICE_NAME in the entity descriptions.
assert name is not DEVICE_CLASS_NAME
assert name is not DEVICE_NAME
self._attr_name = f"{self.device.display_name} {name.title()}"
self._attr_attribution = DEFAULT_ATTRIBUTION

View file

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

View file

@ -53,6 +53,15 @@ SOURCE_CONFIG_ENTRY = "config_entry"
SOURCE_PLATFORM_CONFIG = "platform_config"
class DeviceName(Enum):
"""Singleton to use device name."""
_singleton = 0
DEVICE_NAME = DeviceName._singleton # pylint: disable=protected-access
class DeviceClassName(Enum):
"""Singleton to use device class name."""
@ -229,7 +238,7 @@ class EntityDescription:
force_update: bool = False
icon: str | None = None
has_entity_name: bool = False
name: str | DeviceClassName | None = None
name: str | DeviceClassName | DeviceName | None = None
translation_key: str | None = None
unit_of_measurement: str | None = None
@ -263,6 +272,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
# Protect for multiple updates
_update_staged = False
@ -298,7 +310,7 @@ class Entity(ABC):
_attr_extra_state_attributes: MutableMapping[str, Any]
_attr_force_update: bool
_attr_icon: str | None
_attr_name: str | DeviceClassName | None
_attr_name: str | DeviceClassName | DeviceName | None
_attr_should_poll: bool = True
_attr_state: StateType = STATE_UNKNOWN
_attr_supported_features: int | None = None
@ -319,6 +331,66 @@ 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 setting its"
" name to None. Instead, the name should be set to DEVICE_NAME"
", please %s"
),
self.entity_id,
type(self),
report_issue,
)
self._implicit_device_name_reported = True
if not self.has_entity_name:
return False
if hasattr(self, "_attr_name"):
if (name := self._attr_name) is DEVICE_NAME:
return True
if not name:
# Backwards compatibility with setting _attr_name to None to indicate
# device name.
# Deprecated in HA Core 2023.6, remove in HA Core 2023.7
report_implicit_device_name()
return True
return False
if name_translation_key := self.__name_translation_key():
assert self.platform
if name_translation_key in self.platform.platform_translations:
return False
if hasattr(self, "entity_description"):
if (name := self.entity_description.name) is DEVICE_NAME:
return True
if not name:
# Backwards compatibility with setting EntityDescription.name to None
# for device name.
# Deprecated in HA Core 2023.6, remove in HA Core 2023.7
report_implicit_device_name()
return True
return False
if not self.name:
# Backwards compatibility with setting EntityDescription.name to None
# for device name.
# Deprecated in HA Core 2023.6, remove in HA Core 2023.7
report_implicit_device_name()
return True
return False
@property
def has_entity_name(self) -> bool:
"""Return if the name of the entity is describing only the entity itself."""
@ -340,26 +412,41 @@ class Entity(ABC):
)
return self.platform.component_translations.get(name_translation_key)
def __name_translation_key(self) -> str | None:
"""Return translation key for entity name."""
if self.translation_key is None:
return None
assert self.platform
return (
f"component.{self.platform.platform_name}.entity.{self.platform.domain}"
f".{self.translation_key}.name"
)
@property
def name(self) -> str | None:
"""Return the name of the entity."""
"""Return the name of the entity.
Returns None if the name is set to DEVICE_NAME.
"""
if hasattr(self, "_attr_name"):
if self._attr_name is DEVICE_CLASS_NAME:
if (name := self._attr_name) is DEVICE_CLASS_NAME:
return self._device_class_name()
return self._attr_name
if self.translation_key is not None and self.has_entity_name:
if name is DEVICE_NAME:
return None
return name
if self.has_entity_name and (
name_translation_key := self.__name_translation_key()
):
assert self.platform
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:
name: str = self.platform.platform_translations[name_translation_key]
name = self.platform.platform_translations[name_translation_key]
return name
if hasattr(self, "entity_description"):
if self.entity_description.name is DEVICE_CLASS_NAME:
if (name := self.entity_description.name) is DEVICE_CLASS_NAME:
return self._device_class_name()
return self.entity_description.name
if name is DEVICE_NAME:
return None
return name
return None
@property
@ -637,9 +724,9 @@ class Entity(ABC):
):
return self.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}"
return f"{device_entry.name_by_user or device_entry.name} {self.name}"
@callback
def _async_write_ha_state(self) -> None:

View file

@ -125,8 +125,8 @@ class EntityPlatform:
self.entity_namespace = entity_namespace
self.config_entry: config_entries.ConfigEntry | None = None
self.entities: dict[str, Entity] = {}
self.component_translations: dict[str, Any] = {}
self.platform_translations: dict[str, Any] = {}
self.component_translations: dict[str, str] = {}
self.platform_translations: dict[str, str] = {}
self._tasks: list[asyncio.Task[None]] = []
# Stop tracking tasks after setup is completed
self._setup_complete = False
@ -627,7 +627,7 @@ class EntityPlatform:
else:
if device and entity.has_entity_name: # type: ignore[unreachable]
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}"