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):
"""A binary sensor implementation for Abode device."""
_attr_name = None
_device: ABBinarySensor
@property

View File

@ -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,

View File

@ -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):

View File

@ -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 = (

View File

@ -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."""

View File

@ -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,

View File

@ -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:

View File

@ -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}"
)

View File

@ -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:

View File

@ -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

View File

@ -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, ...] = (

View File

@ -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."""

View File

@ -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}"

View File

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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"}