diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 402b636e5d6..a10dbc8e664 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -42,6 +42,7 @@ async def async_setup_entry( class AbodeBinarySensor(AbodeDevice, BinarySensorEntity): """A binary sensor implementation for Abode device.""" + _attr_name = None _device: ABBinarySensor @property diff --git a/homeassistant/components/aranet/sensor.py b/homeassistant/components/aranet/sensor.py index 6ac27b1652b..90448b9c89d 100644 --- a/homeassistant/components/aranet/sensor.py +++ b/homeassistant/components/aranet/sensor.py @@ -1,6 +1,8 @@ """Support for Aranet sensors.""" from __future__ import annotations +from dataclasses import dataclass + from aranet4.client import Aranet4Advertisement from bleak.backends.device import BLEDevice @@ -33,43 +35,54 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN + +@dataclass +class AranetSensorEntityDescription(SensorEntityDescription): + """Class to describe an Aranet sensor entity.""" + + # PassiveBluetoothDataUpdate does not support UNDEFINED + # Restrict the type to satisfy the type checker and catch attempts + # to use UNDEFINED in the entity descriptions. + name: str | None = None + + SENSOR_DESCRIPTIONS = { - "temperature": SensorEntityDescription( + "temperature": AranetSensorEntityDescription( key="temperature", name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), - "humidity": SensorEntityDescription( + "humidity": AranetSensorEntityDescription( key="humidity", name="Humidity", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), - "pressure": SensorEntityDescription( + "pressure": AranetSensorEntityDescription( key="pressure", name="Pressure", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=UnitOfPressure.HPA, state_class=SensorStateClass.MEASUREMENT, ), - "co2": SensorEntityDescription( + "co2": AranetSensorEntityDescription( key="co2", name="Carbon Dioxide", device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, ), - "battery": SensorEntityDescription( + "battery": AranetSensorEntityDescription( key="battery", name="Battery", device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), - "interval": SensorEntityDescription( + "interval": AranetSensorEntityDescription( key="update_interval", name="Update Interval", device_class=SensorDeviceClass.DURATION, diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index d380ee11834..c6f406a5094 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -5,7 +5,6 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta import logging -from typing import cast from yalexs.activity import ( ACTION_DOORBELL_CALL_MISSED, @@ -104,7 +103,16 @@ def _native_datetime() -> datetime: @dataclass -class AugustRequiredKeysMixin: +class AugustBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes August binary_sensor entity.""" + + # AugustBinarySensor does not support UNDEFINED or None, + # restrict the type to str. + name: str = "" + + +@dataclass +class AugustDoorbellRequiredKeysMixin: """Mixin for required keys.""" value_fn: Callable[[AugustData, DoorbellDetail], bool] @@ -112,41 +120,45 @@ class AugustRequiredKeysMixin: @dataclass -class AugustBinarySensorEntityDescription( - BinarySensorEntityDescription, AugustRequiredKeysMixin +class AugustDoorbellBinarySensorEntityDescription( + BinarySensorEntityDescription, AugustDoorbellRequiredKeysMixin ): """Describes August binary_sensor entity.""" + # AugustDoorbellBinarySensor does not support UNDEFINED or None, + # restrict the type to str. + name: str = "" -SENSOR_TYPE_DOOR = BinarySensorEntityDescription( + +SENSOR_TYPE_DOOR = AugustBinarySensorEntityDescription( key="door_open", name="Open", ) -SENSOR_TYPES_DOORBELL: tuple[AugustBinarySensorEntityDescription, ...] = ( - AugustBinarySensorEntityDescription( +SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = ( + AugustDoorbellBinarySensorEntityDescription( key="doorbell_ding", name="Ding", device_class=BinarySensorDeviceClass.OCCUPANCY, value_fn=_retrieve_ding_state, is_time_based=True, ), - AugustBinarySensorEntityDescription( + AugustDoorbellBinarySensorEntityDescription( key="doorbell_motion", name="Motion", device_class=BinarySensorDeviceClass.MOTION, value_fn=_retrieve_motion_state, is_time_based=True, ), - AugustBinarySensorEntityDescription( + AugustDoorbellBinarySensorEntityDescription( key="doorbell_image_capture", name="Image Capture", icon="mdi:file-image", value_fn=_retrieve_image_capture_state, is_time_based=True, ), - AugustBinarySensorEntityDescription( + AugustDoorbellBinarySensorEntityDescription( key="doorbell_online", name="Online", device_class=BinarySensorDeviceClass.CONNECTIVITY, @@ -199,7 +211,10 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.DOOR def __init__( - self, data: AugustData, device: Lock, description: BinarySensorEntityDescription + self, + data: AugustData, + device: Lock, + description: AugustBinarySensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(data, device) @@ -207,9 +222,7 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): self._data = data self._device = device self._attr_name = f"{device.device_name} {description.name}" - self._attr_unique_id = ( - f"{self._device_id}_{cast(str, description.name).lower()}" - ) + self._attr_unique_id = f"{self._device_id}_{description.name.lower()}" @callback def _update_from_data(self): @@ -243,13 +256,13 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): """Representation of an August binary sensor.""" - entity_description: AugustBinarySensorEntityDescription + entity_description: AugustDoorbellBinarySensorEntityDescription def __init__( self, data: AugustData, device: Doorbell, - description: AugustBinarySensorEntityDescription, + description: AugustDoorbellBinarySensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(data, device) @@ -257,9 +270,7 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): self._check_for_off_update_listener = None self._data = data self._attr_name = f"{device.device_name} {description.name}" - self._attr_unique_id = ( - f"{self._device_id}_{cast(str, description.name).lower()}" - ) + self._attr_unique_id = f"{self._device_id}_{description.name.lower()}" @callback def _update_from_data(self): diff --git a/homeassistant/components/balboa/binary_sensor.py b/homeassistant/components/balboa/binary_sensor.py index 11a0cae0a01..9f363746a8f 100644 --- a/homeassistant/components/balboa/binary_sensor.py +++ b/homeassistant/components/balboa/binary_sensor.py @@ -47,6 +47,10 @@ class BalboaBinarySensorEntityDescription( ): """A class that describes Balboa binary sensor entities.""" + # BalboaBinarySensorEntity does not support UNDEFINED or None, + # restrict the type to str. + name: str = "" + FILTER_CYCLE_ICONS = ("mdi:sync", "mdi:sync-off") BINARY_SENSOR_DESCRIPTIONS = ( diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index d99f569ed59..1c2d6d779fb 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -190,6 +190,13 @@ class BinarySensorEntity(Entity): _attr_is_on: bool | None = None _attr_state: None = None + def _default_to_device_class_name(self) -> bool: + """Return True if an unnamed entity should be named by its device class. + + For binary sensors this is True if the entity has a device class. + """ + return self.device_class is not None + @property def device_class(self) -> BinarySensorDeviceClass | None: """Return the class of this entity.""" diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py index 32b76c6fcae..1109cf0d311 100644 --- a/homeassistant/components/bond/button.py +++ b/homeassistant/components/bond/button.py @@ -35,6 +35,10 @@ class BondButtonEntityDescription( ): """Class to describe a Bond Button entity.""" + # BondEntity does not support UNDEFINED, + # restrict the type to str | None + name: str | None = None + STOP_BUTTON = BondButtonEntityDescription( key=Action.STOP, diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py index dc7159001d8..0cf4f0f2346 100644 --- a/homeassistant/components/emonitor/sensor.py +++ b/homeassistant/components/emonitor/sensor.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType +from homeassistant.helpers.typing import UNDEFINED, StateType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -80,7 +80,7 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): mac_address = self.emonitor_status.network.mac_address device_name = name_short_mac(mac_address[-6:]) label = self.channel_data.label or f"{device_name} {channel_number}" - if description.name: + if description.name is not UNDEFINED: self._attr_name = f"{label} {description.name}" self._attr_unique_id = f"{mac_address}_{channel_number}_{description.key}" else: diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 2870a61d9a0..44ffbcdb497 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import UnitOfPower from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import UNDEFINED from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -168,7 +169,7 @@ class EnvoyInverter(CoordinatorEntity, SensorEntity): """Initialize Envoy inverter entity.""" self.entity_description = description self._serial_number = serial_number - if description.name: + if description.name is not UNDEFINED: self._attr_name = ( f"{envoy_name} Inverter {serial_number} {description.name}" ) diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py index 01f98b996dd..68c11ad6e1f 100644 --- a/homeassistant/components/gree/switch.py +++ b/homeassistant/components/gree/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any, cast +from typing import Any from greeclimate.device import Device @@ -33,6 +33,10 @@ class GreeRequiredKeysMixin: class GreeSwitchEntityDescription(SwitchEntityDescription, GreeRequiredKeysMixin): """Describes Gree switch entity.""" + # GreeSwitch does not support UNDEFINED or None, + # restrict the type to str. + name: str = "" + def _set_light(device: Device, value: bool) -> None: """Typed helper to set device light property.""" @@ -130,7 +134,7 @@ class GreeSwitch(GreeEntity, SwitchEntity): """Initialize the Gree device.""" self.entity_description = description - super().__init__(coordinator, cast(str, description.name)) + super().__init__(coordinator, description.name) @property def is_on(self) -> bool: diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index f63cc4aac39..133b569c751 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -117,6 +117,10 @@ class HuaweiSensorGroup: class HuaweiSensorEntityDescription(SensorEntityDescription): """Class describing Huawei LTE sensor entities.""" + # HuaweiLteSensor does not support UNDEFINED or None, + # restrict the type to str. + name: str = "" + format_fn: Callable[[str], tuple[StateType, str | None]] = format_default icon_fn: Callable[[StateType], str] | None = None device_class_fn: Callable[[StateType], SensorDeviceClass | None] | None = None diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index b1b391aaaab..9e8cabbe253 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -28,6 +28,9 @@ class IncomfortSensorEntityDescription(SensorEntityDescription): """Describes Incomfort sensor entity.""" extra_key: str | None = None + # IncomfortSensor does not support UNDEFINED or None, + # restrict the type to str + name: str = "" SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = ( diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index a150c052678..62ae375736d 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -1,6 +1,7 @@ """Support for ISY switches.""" from __future__ import annotations +from dataclasses import dataclass from typing import Any from pyisy.constants import ( @@ -22,7 +23,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -30,6 +31,15 @@ from .entity import ISYAuxControlEntity, ISYNodeEntity, ISYProgramEntity from .models import IsyData +@dataclass +class ISYSwitchEntityDescription(SwitchEntityDescription): + """Describes IST switch.""" + + # ISYEnableSwitchEntity does not support UNDEFINED or None, + # restrict the type to str. + name: str = "" + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -53,7 +63,7 @@ async def async_setup_entry( for node, control in isy_data.aux_properties[Platform.SWITCH]: # Currently only used for enable switches, will need to be updated for # NS support by making sure control == TAG_ENABLED - description = SwitchEntityDescription( + description = ISYSwitchEntityDescription( key=control, device_class=SwitchDeviceClass.SWITCH, name=control.title(), @@ -135,7 +145,7 @@ class ISYEnableSwitchEntity(ISYAuxControlEntity, SwitchEntity): node: Node, control: str, unique_id: str, - description: EntityDescription, + description: ISYSwitchEntityDescription, device_info: DeviceInfo | None, ) -> None: """Initialize the ISY Aux Control Number entity.""" diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index 24c97c74b0f..784555e6c73 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntity 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 ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType from . import REPETIER_API, SENSOR_TYPES, UPDATE_SIGNAL, RepetierSensorEntityDescription @@ -45,7 +45,8 @@ def setup_platform( sensor_type = info["sensor_type"] temp_id = info["temp_id"] description = SENSOR_TYPES[sensor_type] - name = f"{info['name']}{description.name or ''}" + name_suffix = "" if description.name is UNDEFINED else description.name + name = f"{info['name']}{name_suffix}" if temp_id is not None: _LOGGER.debug("%s Temp_id: %s", sensor_type, temp_id) name = f"{name}{temp_id}" diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index b729138f73e..03cf65a49ff 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -130,6 +130,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): """ _attr_force_update = True + _attr_name = None def __init__( self, diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 50d41899800..daffcb006d1 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -275,6 +275,10 @@ def async_setup_entry_rest( class BlockEntityDescription(EntityDescription): """Class to describe a BLOCK entity.""" + # BlockEntity does not support UNDEFINED or None, + # restrict the type to str. + name: str = "" + icon_fn: Callable[[dict], str] | None = None unit_fn: Callable[[dict], str] | None = None value: Callable[[Any], Any] = lambda val: val @@ -295,6 +299,10 @@ class RpcEntityRequiredKeysMixin: class RpcEntityDescription(EntityDescription, RpcEntityRequiredKeysMixin): """Class to describe a RPC entity.""" + # BlockEntity does not support UNDEFINED or None, + # restrict the type to str. + name: str = "" + value: Callable[[Any, Any], Any] | None = None available: Callable[[dict], bool] | None = None removal_condition: Callable[[dict, dict, str], bool] | None = None @@ -307,6 +315,10 @@ class RpcEntityDescription(EntityDescription, RpcEntityRequiredKeysMixin): class RestEntityDescription(EntityDescription): """Class to describe a REST entity.""" + # BlockEntity does not support UNDEFINED or None, + # restrict the type to str. + name: str = "" + value: Callable[[dict, Any], Any] | None = None extra_state_attributes: Callable[[dict], dict | None] | None = None diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index 0a0cf40ca8b..cb11c64f16a 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -94,7 +94,6 @@ class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity): self._sensor = binary_sensor self._attr_unique_id = f"{coordinator.base_unique_id}-{binary_sensor}" self.entity_description = BINARY_SENSOR_TYPES[binary_sensor] - self._attr_name = self.entity_description.name @property def is_on(self) -> bool: diff --git a/homeassistant/components/system_bridge/binary_sensor.py b/homeassistant/components/system_bridge/binary_sensor.py index bb83d90235f..5c23c3110d8 100644 --- a/homeassistant/components/system_bridge/binary_sensor.py +++ b/homeassistant/components/system_bridge/binary_sensor.py @@ -23,6 +23,10 @@ from .coordinator import SystemBridgeDataUpdateCoordinator class SystemBridgeBinarySensorEntityDescription(BinarySensorEntityDescription): """Class describing System Bridge binary sensor entities.""" + # SystemBridgeBinarySensor does not support UNDEFINED or None, + # restrict the type to str. + name: str = "" + value: Callable = round diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index a6bf29ac546..ede94863af4 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -46,6 +46,10 @@ PIXELS: Final = "px" class SystemBridgeSensorEntityDescription(SensorEntityDescription): """Class describing System Bridge sensor entities.""" + # SystemBridgeSensor does not support UNDEFINED or None, + # restrict the type to str. + name: str = "" + value: Callable = round diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py index 2b7d466d2f0..046dc79f2c6 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -72,6 +72,10 @@ from .const import ( class TomorrowioSensorEntityDescription(SensorEntityDescription): """Describes a Tomorrow.io sensor entity.""" + # TomorrowioSensor does not support UNDEFINED or None, + # restrict the type to str. + name: str = "" + unit_imperial: str | None = None unit_metric: str | None = None multiplication_factor: Callable[[float], float] | float | None = None diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 81cce80aa73..1b3839ce2d7 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -24,6 +24,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import UNDEFINED from .base_class import TradfriBaseEntity from .const import ( @@ -202,7 +203,7 @@ class TradfriSensor(TradfriBaseEntity, SensorEntity): self._attr_unique_id = f"{self._attr_unique_id}-{description.key}" - if description.name: + if description.name is not UNDEFINED: self._attr_name = f"{self._attr_name}: {description.name}" self._refresh() # Set initial state diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index e123a4bf1bc..15bd17554ad 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -23,6 +23,7 @@ from pyunifiprotect.data import ( from homeassistant.core import callback import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.typing import UNDEFINED from .const import ( ATTR_EVENT_ID, @@ -201,7 +202,11 @@ class ProtectDeviceEntity(Entity): else: self.entity_description = description self._attr_unique_id = f"{self.device.mac}_{description.key}" - name = description.name or "" + name = ( + description.name + if description.name and description.name is not UNDEFINED + else "" + ) self._attr_name = f"{self.device.display_name} {name.title()}" if isinstance(description, ProtectRequiredKeysMixin): self._async_get_ufp_enabled = description.get_ufp_enabled diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py index 634d4a9e41d..15e396cc660 100644 --- a/homeassistant/components/wemo/sensor.py +++ b/homeassistant/components/wemo/sensor.py @@ -28,6 +28,9 @@ from .wemo_device import DeviceCoordinator class AttributeSensorDescription(SensorEntityDescription): """SensorEntityDescription for WeMo AttributeSensor entities.""" + # AttributeSensor does not support UNDEFINED, + # restrict the type to str | None. + name: str | None = None state_conversion: Callable[[StateType], StateType] | None = None unique_id_suffix: str | None = None diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index ba086b3e8bf..2a0f5ff4e72 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -14,6 +14,7 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.typing import UNDEFINED from .const import DOMAIN, LOGGER from .discovery import ZwaveDiscoveryInfo @@ -161,6 +162,7 @@ class ZWaveBaseEntity(Entity): hasattr(self, "entity_description") and self.entity_description and self.entity_description.name + and self.entity_description.name is not UNDEFINED ): name = self.entity_description.name diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 316c0b81eeb..f3568588287 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -48,7 +48,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType +from homeassistant.helpers.typing import UNDEFINED, StateType from .const import ( ATTR_METER_TYPE, @@ -610,7 +610,7 @@ class ZwaveSensor(ZWaveBaseEntity, SensorEntity): # Entity class attributes self._attr_force_update = True - if not entity_description.name: + if not entity_description.name or entity_description.name is UNDEFINED: self._attr_name = self.generate_name(include_value_name=True) @property diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 68f64f0c749..cb947ac7604 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -44,7 +44,7 @@ from .event import ( async_track_device_registry_updated_event, async_track_entity_registry_updated_event, ) -from .typing import StateType +from .typing import UNDEFINED, StateType, UndefinedType if TYPE_CHECKING: from .entity_platform import EntityPlatform @@ -222,7 +222,7 @@ class EntityDescription: force_update: bool = False icon: str | None = None has_entity_name: bool = False - name: str | None = None + name: str | UndefinedType | None = UNDEFINED translation_key: str | None = None unit_of_measurement: str | None = None @@ -328,6 +328,22 @@ class Entity(ABC): return self.entity_description.has_entity_name 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) + + def _default_to_device_class_name(self) -> bool: + """Return True if an unnamed entity should be named by its device class.""" + return False + @property def name(self) -> str | None: """Return the name of the entity.""" @@ -338,11 +354,21 @@ class Entity(ABC): f"component.{self.platform.platform_name}.entity.{self.platform.domain}" f".{self.translation_key}.name" ) - if name_translation_key in self.platform.entity_translations: - name: str = self.platform.entity_translations[name_translation_key] + if name_translation_key in self.platform.platform_translations: + name: str = self.platform.platform_translations[name_translation_key] return name if hasattr(self, "entity_description"): - return self.entity_description.name + 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 + + # 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 @property diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index f7793a4329c..ddc741b7d35 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -126,7 +126,8 @@ class EntityPlatform: self.entity_namespace = entity_namespace self.config_entry: config_entries.ConfigEntry | None = None 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]] = [] # Stop tracking tasks after setup is completed self._setup_complete = False @@ -295,7 +296,15 @@ class EntityPlatform: full_name = f"{self.domain}.{self.platform_name}" 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} ) except Exception as err: # pylint: disable=broad-exception-caught diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index 79596d95d98..df377cd09d1 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -1,8 +1,25 @@ """The tests for the Binary sensor component.""" +from collections.abc import Generator from unittest import mock +import pytest + from homeassistant.components import binary_sensor +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from tests.common import ( + MockConfigEntry, + MockModule, + MockPlatform, + mock_config_flow, + mock_integration, + mock_platform, +) + +TEST_DOMAIN = "test" def test_state() -> None: @@ -19,3 +36,93 @@ def test_state() -> None: new=True, ): assert binary_sensor.BinarySensorEntity().state == STATE_ON + + +class STTFlow(ConfigFlow): + """Test flow.""" + + +@pytest.fixture(autouse=True) +def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: + """Mock config flow.""" + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + + with mock_config_flow(TEST_DOMAIN, STTFlow): + yield + + +async def test_name(hass: HomeAssistant) -> None: + """Test binary sensor name.""" + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setup( + config_entry, binary_sensor.DOMAIN + ) + return True + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + mock_integration( + hass, + MockModule( + TEST_DOMAIN, + async_setup_entry=async_setup_entry_init, + ), + ) + + # Unnamed binary sensor without device class -> no name + entity1 = binary_sensor.BinarySensorEntity() + entity1.entity_id = "binary_sensor.test1" + + # Unnamed binary sensor with device class but has_entity_name False -> no name + entity2 = binary_sensor.BinarySensorEntity() + entity2.entity_id = "binary_sensor.test2" + entity2._attr_device_class = binary_sensor.BinarySensorDeviceClass.BATTERY + + # Unnamed binary sensor with device class and has_entity_name True -> named + entity3 = binary_sensor.BinarySensorEntity() + entity3.entity_id = "binary_sensor.test3" + entity3._attr_device_class = binary_sensor.BinarySensorDeviceClass.BATTERY + entity3._attr_has_entity_name = True + + # Unnamed binary sensor with device class and has_entity_name True -> named + entity4 = binary_sensor.BinarySensorEntity() + entity4.entity_id = "binary_sensor.test4" + entity4.entity_description = binary_sensor.BinarySensorEntityDescription( + "test", + binary_sensor.BinarySensorDeviceClass.BATTERY, + has_entity_name=True, + ) + + async def async_setup_entry_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test stt platform via config entry.""" + async_add_entities([entity1, entity2, entity3, entity4]) + + mock_platform( + hass, + f"{TEST_DOMAIN}.{binary_sensor.DOMAIN}", + MockPlatform(async_setup_entry=async_setup_entry_platform), + ) + + config_entry = MockConfigEntry(domain=TEST_DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity1.entity_id) + assert state.attributes == {} + + state = hass.states.get(entity2.entity_id) + assert state.attributes == {"device_class": "battery"} + + state = hass.states.get(entity3.entity_id) + assert state.attributes == {"device_class": "battery", "friendly_name": "Battery"} + + state = hass.states.get(entity4.entity_id) + assert state.attributes == {"device_class": "battery", "friendly_name": "Battery"}