Skip to content

Commit

Permalink
Clean up default ZHA entity names (home-assistant#91841)
Browse files Browse the repository at this point in the history
* Always use `Light` for lights, including subclasses

* Clean up other platforms

* Add a unit test to ensure all future entity classes have names

* Remove stale `_name`

* Address review feedback and rename `Open` to `Opening`
  • Loading branch information
puddly authored Apr 25, 2023
1 parent da05763 commit 6842cdc
Show file tree
Hide file tree
Showing 18 changed files with 180 additions and 79 deletions.
1 change: 1 addition & 0 deletions homeassistant/components/zha/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ async def async_setup_entry(
class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity):
"""Entity for ZHA alarm control devices."""

_attr_name: str = "Alarm control panel"
_attr_code_format = CodeFormat.TEXT
_attr_supported_features = (
AlarmControlPanelEntityFeature.ARM_HOME
Expand Down
39 changes: 31 additions & 8 deletions homeassistant/components/zha/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,22 @@
from .entity import ZhaEntity

# Zigbee Cluster Library Zone Type to Home Assistant device class
CLASS_MAPPING = {
0x000D: BinarySensorDeviceClass.MOTION,
0x0015: BinarySensorDeviceClass.OPENING,
0x0028: BinarySensorDeviceClass.SMOKE,
0x002A: BinarySensorDeviceClass.MOISTURE,
0x002B: BinarySensorDeviceClass.GAS,
0x002D: BinarySensorDeviceClass.VIBRATION,
IAS_ZONE_CLASS_MAPPING = {
IasZone.ZoneType.Motion_Sensor: BinarySensorDeviceClass.MOTION,
IasZone.ZoneType.Contact_Switch: BinarySensorDeviceClass.OPENING,
IasZone.ZoneType.Fire_Sensor: BinarySensorDeviceClass.SMOKE,
IasZone.ZoneType.Water_Sensor: BinarySensorDeviceClass.MOISTURE,
IasZone.ZoneType.Carbon_Monoxide_Sensor: BinarySensorDeviceClass.GAS,
IasZone.ZoneType.Vibration_Movement_Sensor: BinarySensorDeviceClass.VIBRATION,
}

IAS_ZONE_NAME_MAPPING = {
IasZone.ZoneType.Motion_Sensor: "Motion",
IasZone.ZoneType.Contact_Switch: "Opening",
IasZone.ZoneType.Fire_Sensor: "Smoke",
IasZone.ZoneType.Water_Sensor: "Moisture",
IasZone.ZoneType.Carbon_Monoxide_Sensor: "Gas",
IasZone.ZoneType.Vibration_Movement_Sensor: "Vibration",
}

STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.BINARY_SENSOR)
Expand Down Expand Up @@ -108,6 +117,7 @@ class Accelerometer(BinarySensor):
"""ZHA BinarySensor."""

SENSOR_ATTR = "acceleration"
_attr_name: str = "Accelerometer"
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.MOVING


Expand All @@ -116,6 +126,7 @@ class Occupancy(BinarySensor):
"""ZHA BinarySensor."""

SENSOR_ATTR = "occupancy"
_attr_name: str = "Occupancy"
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OCCUPANCY


Expand All @@ -124,6 +135,7 @@ class Opening(BinarySensor):
"""ZHA OnOff BinarySensor."""

SENSOR_ATTR = "on_off"
_attr_name: str = "Opening"
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OPENING

# Client/out cluster attributes aren't stored in the zigpy database, but are properly stored in the runtime cache.
Expand All @@ -142,6 +154,7 @@ class BinaryInput(BinarySensor):
"""ZHA BinarySensor."""

SENSOR_ATTR = "present_value"
_attr_name: str = "Binary input"


@STRICT_MATCH(
Expand All @@ -159,6 +172,7 @@ class BinaryInput(BinarySensor):
class Motion(Opening):
"""ZHA OnOff BinarySensor with motion device class."""

_attr_name: str = "Motion"
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.MOTION


Expand All @@ -168,10 +182,17 @@ class IASZone(BinarySensor):

SENSOR_ATTR = "zone_status"

@property
def name(self) -> str | None:
"""Return the name of the sensor."""
zone_type = self._cluster_handler.cluster.get("zone_type")
return IAS_ZONE_NAME_MAPPING.get(zone_type, "iaszone")

@property
def device_class(self) -> BinarySensorDeviceClass | None:
"""Return device class from component DEVICE_CLASSES."""
return CLASS_MAPPING.get(self._cluster_handler.cluster.get("zone_type"))
zone_type = self._cluster_handler.cluster.get("zone_type")
return IAS_ZONE_CLASS_MAPPING.get(zone_type)

@staticmethod
def parse(value: bool | int) -> bool:
Expand Down Expand Up @@ -220,6 +241,7 @@ class FrostLock(BinarySensor, id_suffix="frost_lock"):

SENSOR_ATTR = "frost_lock"
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.LOCK
_attr_name: str = "Frost lock"


@MULTI_MATCH(cluster_handler_names="ikea_airpurifier")
Expand All @@ -228,6 +250,7 @@ class ReplaceFilter(BinarySensor, id_suffix="replace_filter"):

SENSOR_ATTR = "replace_filter"
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PROBLEM
_attr_name: str = "Replace filter"


@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"})
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/zha/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ class Thermostat(ZhaEntity, ClimateEntity):

_attr_precision = PRECISION_TENTHS
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_name: str = "Thermostat"

def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
"""Initialize ZHA Thermostat instance."""
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/zha/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ async def async_setup_entry(
class ZhaCover(ZhaEntity, CoverEntity):
"""Representation of a ZHA cover."""

_attr_name: str = "Cover"

def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
"""Init this sensor."""
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
Expand Down Expand Up @@ -197,6 +199,7 @@ class Shade(ZhaEntity, CoverEntity):
"""ZHA Shade."""

_attr_device_class = CoverDeviceClass.SHADE
_attr_name: str = "Shade"

def __init__(
self,
Expand Down Expand Up @@ -308,6 +311,8 @@ async def async_stop_cover(self, **kwargs: Any) -> None:
class KeenVent(Shade):
"""Keen vent cover."""

_attr_name: str = "Keen vent"

_attr_device_class = CoverDeviceClass.DAMPER

async def async_open_cover(self, **kwargs: Any) -> None:
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/zha/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity):
"""Represent a tracked device."""

_attr_should_poll = True # BaseZhaEntity defaults to False
_attr_name: str = "Device scanner"

def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
"""Initialize the ZHA device tracker."""
Expand Down
24 changes: 6 additions & 18 deletions homeassistant/components/zha/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ class BaseZhaEntity(LogMixin, entity.Entity):

def __init__(self, unique_id: str, zha_device: ZHADevice, **kwargs: Any) -> None:
"""Init ZHA entity."""
self._name: str = ""
self._unique_id: str = unique_id
if self.unique_id_suffix:
self._unique_id += f"-{self.unique_id_suffix}"
Expand All @@ -62,13 +61,6 @@ def __init__(self, unique_id: str, zha_device: ZHADevice, **kwargs: Any) -> None
self._unsubs: list[Callable[[], None]] = []
self.remove_future: asyncio.Future[Any] = asyncio.Future()

@property
def name(self) -> str:
"""Return Entity's default name."""
if hasattr(self, "_attr_name") and self._attr_name is not None:
return self._attr_name
return self._name

@property
def unique_id(self) -> str:
"""Return a unique ID."""
Expand Down Expand Up @@ -167,13 +159,7 @@ def __init__(
) -> None:
"""Init ZHA entity."""
super().__init__(unique_id, zha_device, **kwargs)
self._name: str = (
self.__class__.__name__.lower()
.replace("zha", "")
.replace("entity", "")
.replace("sensor", "")
.capitalize()
)

self.cluster_handlers: dict[str, ClusterHandler] = {}
for cluster_handler in cluster_handlers:
self.cluster_handlers[cluster_handler.name] = cluster_handler
Expand Down Expand Up @@ -249,6 +235,9 @@ async def async_update(self) -> None:
class ZhaGroupEntity(BaseZhaEntity):
"""A base class for ZHA group entities."""

# The group name is set in the initializer
_attr_name: str

def __init__(
self,
entity_ids: list[str],
Expand All @@ -261,16 +250,15 @@ def __init__(
super().__init__(unique_id, zha_device, **kwargs)
self._available = False
self._group = zha_device.gateway.groups.get(group_id)
self._name = (
f"{self._group.name}_zha_group_0x{group_id:04x}".lower().capitalize()
)
self._group_id: int = group_id
self._entity_ids: list[str] = entity_ids
self._async_unsub_state_changed: CALLBACK_TYPE | None = None
self._handled_group_membership = False
self._change_listener_debouncer: Debouncer | None = None
self._update_group_from_child_delay = DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY

self._attr_name = self._group.name

@property
def available(self) -> bool:
"""Return entity availability."""
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/zha/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ def async_set_state(self, attr_id, attr_name, value):
class ZhaFan(BaseFan, ZhaEntity):
"""Representation of a ZHA fan."""

_attr_name: str = "Fan"

def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
"""Init this sensor."""
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
Expand Down Expand Up @@ -263,6 +265,8 @@ async def async_added_to_hass(self) -> None:
class IkeaFan(BaseFan, ZhaEntity):
"""Representation of a ZHA fan."""

_attr_name: str = "IKEA fan"

def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
"""Init this sensor."""
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/zha/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ def async_transition_complete(self, _=None) -> None:
class Light(BaseLight, ZhaEntity):
"""Representation of a ZHA or ZLL light."""

_attr_name: str = "Light"
_attr_supported_color_modes: set[ColorMode]
_REFRESH_INTERVAL = (45, 75)

Expand Down Expand Up @@ -1065,6 +1066,7 @@ def _assume_group_state(self, signal, update_params) -> None:
class HueLight(Light):
"""Representation of a HUE light which does not report attributes."""

_attr_name: str = "Light"
_REFRESH_INTERVAL = (3, 5)


Expand All @@ -1076,6 +1078,7 @@ class HueLight(Light):
class ForceOnLight(Light):
"""Representation of a light which does not respect move_to_level_with_on_off."""

_attr_name: str = "Light"
_FORCE_ON = True


Expand All @@ -1087,6 +1090,7 @@ class ForceOnLight(Light):
class MinTransitionLight(Light):
"""Representation of a light which does not react to any "move to" calls with 0 as a transition."""

_attr_name: str = "Light"
_DEFAULT_MIN_TRANSITION_TIME = 1


Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/zha/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ async def async_setup_entry(
class ZhaDoorLock(ZhaEntity, LockEntity):
"""Representation of a ZHA lock."""

_attr_name: str = "Door lock"

def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
"""Init this sensor."""
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/zha/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ async def async_setup_entry(
class ZhaNumber(ZhaEntity, NumberEntity):
"""Representation of a ZHA Number entity."""

_attr_name: str = "Number"

def __init__(
self,
unique_id: str,
Expand Down Expand Up @@ -331,7 +333,7 @@ def native_step(self) -> float | None:
return super().native_step

@property
def name(self) -> str:
def name(self) -> str | None:
"""Return the name of the number entity."""
description = self._analog_output_cluster_handler.description
if description is not None and len(description) > 0:
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/zha/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ class AnalogInput(Sensor):
"""Sensor that displays analog input values."""

SENSOR_ATTR = "present_value"
_attr_name: str = "Analog input"


@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_POWER_CONFIGURATION)
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/zha/siren.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ async def async_setup_entry(
class ZHASiren(ZhaEntity, SirenEntity):
"""Representation of a ZHA siren."""

_attr_name: str = "Siren"

def __init__(
self,
unique_id: str,
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/zha/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ async def async_setup_entry(
class Switch(ZhaEntity, SwitchEntity):
"""ZHA switch."""

_attr_name: str = "Switch"

def __init__(
self,
unique_id: str,
Expand Down Expand Up @@ -286,6 +288,7 @@ class OnOffWindowDetectionFunctionConfigurationEntity(

_zcl_attribute: str = "window_detection_function"
_zcl_inverter_attribute: str = "window_detection_function_inverter"
_attr_name: str = "Invert window detection"


@CONFIG_DIAGNOSTIC_MATCH(
Expand Down
4 changes: 3 additions & 1 deletion tests/components/zha/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ def find_entity_ids(domain, zha_device, hass):

def async_find_group_entity_id(hass, domain, group):
"""Find the group entity id under test."""
entity_id = f"{domain}.fakemanufacturer_fakemodel_{group.name.lower().replace(' ','_')}_zha_group_0x{group.group_id:04x}"
entity_id = (
f"{domain}.fakemanufacturer_fakemodel_{group.name.lower().replace(' ', '_')}"
)

entity_ids = hass.states.async_entity_ids(domain)

Expand Down
1 change: 1 addition & 0 deletions tests/components/zha/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ def _mock_dev(
nwk=0xB79C,
patch_cluster=True,
quirk=None,
attributes=None,
):
"""Make a fake device using the specified cluster classes."""
device = zigpy.device.Device(
Expand Down
10 changes: 6 additions & 4 deletions tests/components/zha/test_discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from .common import get_zha_gateway
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
from .zha_devices_list import (
DEV_SIG_ATTRIBUTES,
DEV_SIG_CLUSTER_HANDLERS,
DEV_SIG_ENT_MAP,
DEV_SIG_ENT_MAP_CLASS,
Expand Down Expand Up @@ -89,11 +90,12 @@ async def test_devices(
entity_registry = er.async_get(hass_disable_services)

zigpy_device = zigpy_device_mock(
device[SIG_ENDPOINTS],
"00:11:22:33:44:55:66:77",
device[SIG_MANUFACTURER],
device[SIG_MODEL],
endpoints=device[SIG_ENDPOINTS],
ieee="00:11:22:33:44:55:66:77",
manufacturer=device[SIG_MANUFACTURER],
model=device[SIG_MODEL],
node_descriptor=device[SIG_NODE_DESC],
attributes=device.get(DEV_SIG_ATTRIBUTES),
patch_cluster=False,
)

Expand Down
Loading

0 comments on commit 6842cdc

Please sign in to comment.