Name unnamed binary sensors by their device class (#92940)

* Name unnamed binary sensors by their device class

* Update type annotations

* Fix loading of entity component translations

* Add test

* Update integrations

* Set abode and rfxtrx binary_sensor name to None

* Revert changes in homekit_controller
This commit is contained in:
Erik Montnemery 2023-06-13 19:48:54 +02:00 committed by GitHub
parent 223394eaee
commit 2406b235b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 287 additions and 47 deletions

View file

@ -42,6 +42,7 @@ async def async_setup_entry(
class AbodeBinarySensor(AbodeDevice, BinarySensorEntity): class AbodeBinarySensor(AbodeDevice, BinarySensorEntity):
"""A binary sensor implementation for Abode device.""" """A binary sensor implementation for Abode device."""
_attr_name = None
_device: ABBinarySensor _device: ABBinarySensor
@property @property

View file

@ -1,6 +1,8 @@
"""Support for Aranet sensors.""" """Support for Aranet sensors."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from aranet4.client import Aranet4Advertisement from aranet4.client import Aranet4Advertisement
from bleak.backends.device import BLEDevice from bleak.backends.device import BLEDevice
@ -33,43 +35,54 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN 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 = { SENSOR_DESCRIPTIONS = {
"temperature": SensorEntityDescription( "temperature": AranetSensorEntityDescription(
key="temperature", key="temperature",
name="Temperature", name="Temperature",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
"humidity": SensorEntityDescription( "humidity": AranetSensorEntityDescription(
key="humidity", key="humidity",
name="Humidity", name="Humidity",
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
"pressure": SensorEntityDescription( "pressure": AranetSensorEntityDescription(
key="pressure", key="pressure",
name="Pressure", name="Pressure",
device_class=SensorDeviceClass.PRESSURE, device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.HPA, native_unit_of_measurement=UnitOfPressure.HPA,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
"co2": SensorEntityDescription( "co2": AranetSensorEntityDescription(
key="co2", key="co2",
name="Carbon Dioxide", name="Carbon Dioxide",
device_class=SensorDeviceClass.CO2, device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
"battery": SensorEntityDescription( "battery": AranetSensorEntityDescription(
key="battery", key="battery",
name="Battery", name="Battery",
device_class=SensorDeviceClass.BATTERY, device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
"interval": SensorEntityDescription( "interval": AranetSensorEntityDescription(
key="update_interval", key="update_interval",
name="Update Interval", name="Update Interval",
device_class=SensorDeviceClass.DURATION, device_class=SensorDeviceClass.DURATION,

View file

@ -5,7 +5,6 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from typing import cast
from yalexs.activity import ( from yalexs.activity import (
ACTION_DOORBELL_CALL_MISSED, ACTION_DOORBELL_CALL_MISSED,
@ -104,7 +103,16 @@ def _native_datetime() -> datetime:
@dataclass @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.""" """Mixin for required keys."""
value_fn: Callable[[AugustData, DoorbellDetail], bool] value_fn: Callable[[AugustData, DoorbellDetail], bool]
@ -112,41 +120,45 @@ class AugustRequiredKeysMixin:
@dataclass @dataclass
class AugustBinarySensorEntityDescription( class AugustDoorbellBinarySensorEntityDescription(
BinarySensorEntityDescription, AugustRequiredKeysMixin BinarySensorEntityDescription, AugustDoorbellRequiredKeysMixin
): ):
"""Describes August binary_sensor entity.""" """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", key="door_open",
name="Open", name="Open",
) )
SENSOR_TYPES_DOORBELL: tuple[AugustBinarySensorEntityDescription, ...] = ( SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = (
AugustBinarySensorEntityDescription( AugustDoorbellBinarySensorEntityDescription(
key="doorbell_ding", key="doorbell_ding",
name="Ding", name="Ding",
device_class=BinarySensorDeviceClass.OCCUPANCY, device_class=BinarySensorDeviceClass.OCCUPANCY,
value_fn=_retrieve_ding_state, value_fn=_retrieve_ding_state,
is_time_based=True, is_time_based=True,
), ),
AugustBinarySensorEntityDescription( AugustDoorbellBinarySensorEntityDescription(
key="doorbell_motion", key="doorbell_motion",
name="Motion", name="Motion",
device_class=BinarySensorDeviceClass.MOTION, device_class=BinarySensorDeviceClass.MOTION,
value_fn=_retrieve_motion_state, value_fn=_retrieve_motion_state,
is_time_based=True, is_time_based=True,
), ),
AugustBinarySensorEntityDescription( AugustDoorbellBinarySensorEntityDescription(
key="doorbell_image_capture", key="doorbell_image_capture",
name="Image Capture", name="Image Capture",
icon="mdi:file-image", icon="mdi:file-image",
value_fn=_retrieve_image_capture_state, value_fn=_retrieve_image_capture_state,
is_time_based=True, is_time_based=True,
), ),
AugustBinarySensorEntityDescription( AugustDoorbellBinarySensorEntityDescription(
key="doorbell_online", key="doorbell_online",
name="Online", name="Online",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
@ -199,7 +211,10 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.DOOR _attr_device_class = BinarySensorDeviceClass.DOOR
def __init__( def __init__(
self, data: AugustData, device: Lock, description: BinarySensorEntityDescription self,
data: AugustData,
device: Lock,
description: AugustBinarySensorEntityDescription,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(data, device) super().__init__(data, device)
@ -207,9 +222,7 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity):
self._data = data self._data = data
self._device = device self._device = device
self._attr_name = f"{device.device_name} {description.name}" self._attr_name = f"{device.device_name} {description.name}"
self._attr_unique_id = ( self._attr_unique_id = f"{self._device_id}_{description.name.lower()}"
f"{self._device_id}_{cast(str, description.name).lower()}"
)
@callback @callback
def _update_from_data(self): def _update_from_data(self):
@ -243,13 +256,13 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity):
class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity):
"""Representation of an August binary sensor.""" """Representation of an August binary sensor."""
entity_description: AugustBinarySensorEntityDescription entity_description: AugustDoorbellBinarySensorEntityDescription
def __init__( def __init__(
self, self,
data: AugustData, data: AugustData,
device: Doorbell, device: Doorbell,
description: AugustBinarySensorEntityDescription, description: AugustDoorbellBinarySensorEntityDescription,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(data, device) super().__init__(data, device)
@ -257,9 +270,7 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity):
self._check_for_off_update_listener = None self._check_for_off_update_listener = None
self._data = data self._data = data
self._attr_name = f"{device.device_name} {description.name}" self._attr_name = f"{device.device_name} {description.name}"
self._attr_unique_id = ( self._attr_unique_id = f"{self._device_id}_{description.name.lower()}"
f"{self._device_id}_{cast(str, description.name).lower()}"
)
@callback @callback
def _update_from_data(self): def _update_from_data(self):

View file

@ -47,6 +47,10 @@ class BalboaBinarySensorEntityDescription(
): ):
"""A class that describes Balboa binary sensor entities.""" """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") FILTER_CYCLE_ICONS = ("mdi:sync", "mdi:sync-off")
BINARY_SENSOR_DESCRIPTIONS = ( BINARY_SENSOR_DESCRIPTIONS = (

View file

@ -190,6 +190,13 @@ class BinarySensorEntity(Entity):
_attr_is_on: bool | None = None _attr_is_on: bool | None = None
_attr_state: 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 @property
def device_class(self) -> BinarySensorDeviceClass | None: def device_class(self) -> BinarySensorDeviceClass | None:
"""Return the class of this entity.""" """Return the class of this entity."""

View file

@ -35,6 +35,10 @@ class BondButtonEntityDescription(
): ):
"""Class to describe a Bond Button entity.""" """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( STOP_BUTTON = BondButtonEntityDescription(
key=Action.STOP, key=Action.STOP,

View file

@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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 ( from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator, DataUpdateCoordinator,
@ -80,7 +80,7 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity):
mac_address = self.emonitor_status.network.mac_address mac_address = self.emonitor_status.network.mac_address
device_name = name_short_mac(mac_address[-6:]) device_name = name_short_mac(mac_address[-6:])
label = self.channel_data.label or f"{device_name} {channel_number}" 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_name = f"{label} {description.name}"
self._attr_unique_id = f"{mac_address}_{channel_number}_{description.key}" self._attr_unique_id = f"{mac_address}_{channel_number}_{description.key}"
else: else:

View file

@ -18,6 +18,7 @@ from homeassistant.const import UnitOfPower
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import UNDEFINED
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator, DataUpdateCoordinator,
@ -168,7 +169,7 @@ class EnvoyInverter(CoordinatorEntity, SensorEntity):
"""Initialize Envoy inverter entity.""" """Initialize Envoy inverter entity."""
self.entity_description = description self.entity_description = description
self._serial_number = serial_number self._serial_number = serial_number
if description.name: if description.name is not UNDEFINED:
self._attr_name = ( self._attr_name = (
f"{envoy_name} Inverter {serial_number} {description.name}" f"{envoy_name} Inverter {serial_number} {description.name}"
) )

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, cast from typing import Any
from greeclimate.device import Device from greeclimate.device import Device
@ -33,6 +33,10 @@ class GreeRequiredKeysMixin:
class GreeSwitchEntityDescription(SwitchEntityDescription, GreeRequiredKeysMixin): class GreeSwitchEntityDescription(SwitchEntityDescription, GreeRequiredKeysMixin):
"""Describes Gree switch entity.""" """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: def _set_light(device: Device, value: bool) -> None:
"""Typed helper to set device light property.""" """Typed helper to set device light property."""
@ -130,7 +134,7 @@ class GreeSwitch(GreeEntity, SwitchEntity):
"""Initialize the Gree device.""" """Initialize the Gree device."""
self.entity_description = description self.entity_description = description
super().__init__(coordinator, cast(str, description.name)) super().__init__(coordinator, description.name)
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:

View file

@ -117,6 +117,10 @@ class HuaweiSensorGroup:
class HuaweiSensorEntityDescription(SensorEntityDescription): class HuaweiSensorEntityDescription(SensorEntityDescription):
"""Class describing Huawei LTE sensor entities.""" """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 format_fn: Callable[[str], tuple[StateType, str | None]] = format_default
icon_fn: Callable[[StateType], str] | None = None icon_fn: Callable[[StateType], str] | None = None
device_class_fn: Callable[[StateType], SensorDeviceClass | None] | None = None device_class_fn: Callable[[StateType], SensorDeviceClass | None] | None = None

View file

@ -28,6 +28,9 @@ 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 UNDEFINED or None,
# restrict the type to str
name: str = ""
SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = ( SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = (

View file

@ -1,6 +1,7 @@
"""Support for ISY switches.""" """Support for ISY switches."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from typing import Any from typing import Any
from pyisy.constants import ( from pyisy.constants import (
@ -22,7 +23,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory, Platform from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError 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 homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
@ -30,6 +31,15 @@ from .entity import ISYAuxControlEntity, ISYNodeEntity, ISYProgramEntity
from .models import IsyData 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( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
@ -53,7 +63,7 @@ async def async_setup_entry(
for node, control in isy_data.aux_properties[Platform.SWITCH]: for node, control in isy_data.aux_properties[Platform.SWITCH]:
# Currently only used for enable switches, will need to be updated for # Currently only used for enable switches, will need to be updated for
# NS support by making sure control == TAG_ENABLED # NS support by making sure control == TAG_ENABLED
description = SwitchEntityDescription( description = ISYSwitchEntityDescription(
key=control, key=control,
device_class=SwitchDeviceClass.SWITCH, device_class=SwitchDeviceClass.SWITCH,
name=control.title(), name=control.title(),
@ -135,7 +145,7 @@ class ISYEnableSwitchEntity(ISYAuxControlEntity, SwitchEntity):
node: Node, node: Node,
control: str, control: str,
unique_id: str, unique_id: str,
description: EntityDescription, description: ISYSwitchEntityDescription,
device_info: DeviceInfo | None, device_info: DeviceInfo | None,
) -> None: ) -> None:
"""Initialize the ISY Aux Control Number entity.""" """Initialize the ISY Aux Control Number entity."""

View file

@ -9,7 +9,7 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
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 ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType
from . import REPETIER_API, SENSOR_TYPES, UPDATE_SIGNAL, RepetierSensorEntityDescription from . import REPETIER_API, SENSOR_TYPES, UPDATE_SIGNAL, RepetierSensorEntityDescription
@ -45,7 +45,8 @@ def setup_platform(
sensor_type = info["sensor_type"] sensor_type = info["sensor_type"]
temp_id = info["temp_id"] temp_id = info["temp_id"]
description = SENSOR_TYPES[sensor_type] 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: if temp_id is not None:
_LOGGER.debug("%s Temp_id: %s", sensor_type, temp_id) _LOGGER.debug("%s Temp_id: %s", sensor_type, temp_id)
name = f"{name}{temp_id}" name = f"{name}{temp_id}"

View file

@ -130,6 +130,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity):
""" """
_attr_force_update = True _attr_force_update = True
_attr_name = None
def __init__( def __init__(
self, self,

View file

@ -275,6 +275,10 @@ def async_setup_entry_rest(
class BlockEntityDescription(EntityDescription): class BlockEntityDescription(EntityDescription):
"""Class to describe a BLOCK entity.""" """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 icon_fn: Callable[[dict], str] | None = None
unit_fn: Callable[[dict], str] | None = None unit_fn: Callable[[dict], str] | None = None
value: Callable[[Any], Any] = lambda val: val value: Callable[[Any], Any] = lambda val: val
@ -295,6 +299,10 @@ class RpcEntityRequiredKeysMixin:
class RpcEntityDescription(EntityDescription, RpcEntityRequiredKeysMixin): class RpcEntityDescription(EntityDescription, RpcEntityRequiredKeysMixin):
"""Class to describe a RPC entity.""" """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 value: Callable[[Any, Any], Any] | None = None
available: Callable[[dict], bool] | None = None available: Callable[[dict], bool] | None = None
removal_condition: Callable[[dict, dict, str], bool] | None = None removal_condition: Callable[[dict, dict, str], bool] | None = None
@ -307,6 +315,10 @@ class RpcEntityDescription(EntityDescription, RpcEntityRequiredKeysMixin):
class RestEntityDescription(EntityDescription): class RestEntityDescription(EntityDescription):
"""Class to describe a REST entity.""" """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 value: Callable[[dict, Any], Any] | None = None
extra_state_attributes: Callable[[dict], dict | None] | None = None extra_state_attributes: Callable[[dict], dict | None] | None = None

View file

@ -94,7 +94,6 @@ class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity):
self._sensor = binary_sensor self._sensor = binary_sensor
self._attr_unique_id = f"{coordinator.base_unique_id}-{binary_sensor}" self._attr_unique_id = f"{coordinator.base_unique_id}-{binary_sensor}"
self.entity_description = BINARY_SENSOR_TYPES[binary_sensor] self.entity_description = BINARY_SENSOR_TYPES[binary_sensor]
self._attr_name = self.entity_description.name
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:

View file

@ -23,6 +23,10 @@ from .coordinator import SystemBridgeDataUpdateCoordinator
class SystemBridgeBinarySensorEntityDescription(BinarySensorEntityDescription): class SystemBridgeBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Class describing System Bridge binary sensor entities.""" """Class describing System Bridge binary sensor entities."""
# SystemBridgeBinarySensor does not support UNDEFINED or None,
# restrict the type to str.
name: str = ""
value: Callable = round value: Callable = round

View file

@ -46,6 +46,10 @@ PIXELS: Final = "px"
class SystemBridgeSensorEntityDescription(SensorEntityDescription): class SystemBridgeSensorEntityDescription(SensorEntityDescription):
"""Class describing System Bridge sensor entities.""" """Class describing System Bridge sensor entities."""
# SystemBridgeSensor does not support UNDEFINED or None,
# restrict the type to str.
name: str = ""
value: Callable = round value: Callable = round

View file

@ -72,6 +72,10 @@ from .const import (
class TomorrowioSensorEntityDescription(SensorEntityDescription): class TomorrowioSensorEntityDescription(SensorEntityDescription):
"""Describes a Tomorrow.io sensor entity.""" """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_imperial: str | None = None
unit_metric: str | None = None unit_metric: str | None = None
multiplication_factor: Callable[[float], float] | float | None = None multiplication_factor: Callable[[float], float] | float | None = None

View file

@ -24,6 +24,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import UNDEFINED
from .base_class import TradfriBaseEntity from .base_class import TradfriBaseEntity
from .const import ( from .const import (
@ -202,7 +203,7 @@ class TradfriSensor(TradfriBaseEntity, SensorEntity):
self._attr_unique_id = f"{self._attr_unique_id}-{description.key}" 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._attr_name = f"{self._attr_name}: {description.name}"
self._refresh() # Set initial state self._refresh() # Set initial state

View file

@ -23,6 +23,7 @@ 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 DeviceInfo, Entity, EntityDescription
from homeassistant.helpers.typing import UNDEFINED
from .const import ( from .const import (
ATTR_EVENT_ID, ATTR_EVENT_ID,
@ -201,7 +202,11 @@ class ProtectDeviceEntity(Entity):
else: else:
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
if description.name and description.name is not UNDEFINED
else ""
)
self._attr_name = f"{self.device.display_name} {name.title()}" self._attr_name = f"{self.device.display_name} {name.title()}"
if isinstance(description, ProtectRequiredKeysMixin): if isinstance(description, ProtectRequiredKeysMixin):
self._async_get_ufp_enabled = description.get_ufp_enabled self._async_get_ufp_enabled = description.get_ufp_enabled

View file

@ -28,6 +28,9 @@ 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 UNDEFINED,
# restrict the type to str | None.
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

@ -14,6 +14,7 @@ from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
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 DeviceInfo, Entity
from homeassistant.helpers.typing import UNDEFINED
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
from .discovery import ZwaveDiscoveryInfo from .discovery import ZwaveDiscoveryInfo
@ -161,6 +162,7 @@ class ZWaveBaseEntity(Entity):
hasattr(self, "entity_description") hasattr(self, "entity_description")
and self.entity_description and self.entity_description
and self.entity_description.name and self.entity_description.name
and self.entity_description.name is not UNDEFINED
): ):
name = self.entity_description.name name = self.entity_description.name

View file

@ -48,7 +48,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform from homeassistant.helpers import entity_platform
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 StateType from homeassistant.helpers.typing import UNDEFINED, StateType
from .const import ( from .const import (
ATTR_METER_TYPE, ATTR_METER_TYPE,
@ -610,7 +610,7 @@ class ZwaveSensor(ZWaveBaseEntity, SensorEntity):
# Entity class attributes # Entity class attributes
self._attr_force_update = True 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) self._attr_name = self.generate_name(include_value_name=True)
@property @property

View file

@ -44,7 +44,7 @@ from .event import (
async_track_device_registry_updated_event, async_track_device_registry_updated_event,
async_track_entity_registry_updated_event, async_track_entity_registry_updated_event,
) )
from .typing import StateType from .typing import UNDEFINED, StateType, UndefinedType
if TYPE_CHECKING: if TYPE_CHECKING:
from .entity_platform import EntityPlatform from .entity_platform import EntityPlatform
@ -222,7 +222,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 | UndefinedType | None = UNDEFINED
translation_key: str | None = None translation_key: str | None = None
unit_of_measurement: str | None = None unit_of_measurement: str | None = None
@ -328,6 +328,22 @@ 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)
def _default_to_device_class_name(self) -> bool:
"""Return True if an unnamed entity should be named by its device class."""
return False
@property @property
def name(self) -> str | None: def name(self) -> str | None:
"""Return the name of the entity.""" """Return the name of the entity."""
@ -338,11 +354,21 @@ 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"):
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 return None
@property @property

View file

@ -126,7 +126,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
@ -295,7 +296,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

@ -1,8 +1,25 @@
"""The tests for the Binary sensor component.""" """The tests for the Binary sensor component."""
from collections.abc import Generator
from unittest import mock from unittest import mock
import pytest
from homeassistant.components import binary_sensor from homeassistant.components import binary_sensor
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import STATE_OFF, STATE_ON 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: def test_state() -> None:
@ -19,3 +36,93 @@ def test_state() -> None:
new=True, new=True,
): ):
assert binary_sensor.BinarySensorEntity().state == STATE_ON 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"}