From 3a76d92e0fd47e2f9642ffc72c6df491181e5e07 Mon Sep 17 00:00:00 2001
From: Martin Hjelmare <marhje52@gmail.com>
Date: Fri, 29 Oct 2021 06:28:02 +0200
Subject: [PATCH] Add zwave_js binary sensor descriptions (#58641)

---
 .../components/zwave_js/binary_sensor.py      | 385 +++++++++---------
 .../components/zwave_js/test_binary_sensor.py |  10 +-
 2 files changed, 199 insertions(+), 196 deletions(-)

diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py
index 4007064109d..a5883e9bcbf 100644
--- a/homeassistant/components/zwave_js/binary_sensor.py
+++ b/homeassistant/components/zwave_js/binary_sensor.py
@@ -1,8 +1,8 @@
 """Representation of Z-Wave binary sensors."""
 from __future__ import annotations
 
+from dataclasses import dataclass
 import logging
-from typing import TypedDict
 
 from zwave_js_server.client import Client as ZwaveClient
 from zwave_js_server.const import CommandClass
@@ -25,6 +25,7 @@ from homeassistant.components.binary_sensor import (
     DEVICE_CLASS_SOUND,
     DOMAIN as BINARY_SENSOR_DOMAIN,
     BinarySensorEntity,
+    BinarySensorEntityDescription,
 )
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant, callback
@@ -38,186 +39,197 @@ from .entity import ZWaveBaseEntity
 LOGGER = logging.getLogger(__name__)
 
 
-NOTIFICATION_SMOKE_ALARM = 1
-NOTIFICATION_CARBON_MONOOXIDE = 2
-NOTIFICATION_CARBON_DIOXIDE = 3
-NOTIFICATION_HEAT = 4
-NOTIFICATION_WATER = 5
-NOTIFICATION_ACCESS_CONTROL = 6
-NOTIFICATION_HOME_SECURITY = 7
-NOTIFICATION_POWER_MANAGEMENT = 8
-NOTIFICATION_SYSTEM = 9
-NOTIFICATION_EMERGENCY = 10
-NOTIFICATION_CLOCK = 11
-NOTIFICATION_APPLIANCE = 12
-NOTIFICATION_HOME_HEALTH = 13
-NOTIFICATION_SIREN = 14
-NOTIFICATION_WATER_VALVE = 15
-NOTIFICATION_WEATHER = 16
-NOTIFICATION_IRRIGATION = 17
-NOTIFICATION_GAS = 18
+NOTIFICATION_SMOKE_ALARM = "1"
+NOTIFICATION_CARBON_MONOOXIDE = "2"
+NOTIFICATION_CARBON_DIOXIDE = "3"
+NOTIFICATION_HEAT = "4"
+NOTIFICATION_WATER = "5"
+NOTIFICATION_ACCESS_CONTROL = "6"
+NOTIFICATION_HOME_SECURITY = "7"
+NOTIFICATION_POWER_MANAGEMENT = "8"
+NOTIFICATION_SYSTEM = "9"
+NOTIFICATION_EMERGENCY = "10"
+NOTIFICATION_CLOCK = "11"
+NOTIFICATION_APPLIANCE = "12"
+NOTIFICATION_HOME_HEALTH = "13"
+NOTIFICATION_SIREN = "14"
+NOTIFICATION_WATER_VALVE = "15"
+NOTIFICATION_WEATHER = "16"
+NOTIFICATION_IRRIGATION = "17"
+NOTIFICATION_GAS = "18"
 
 
-class NotificationSensorMapping(TypedDict, total=False):
-    """Represent a notification sensor mapping dict type."""
+@dataclass
+class NotificationZWaveJSEntityDescription(BinarySensorEntityDescription):
+    """Represent a Z-Wave JS binary sensor entity description."""
 
-    type: int  # required
-    states: list[str]
-    device_class: str
-    enabled: bool
+    states: tuple[str, ...] | None = None
+
+
+@dataclass
+class PropertyZWaveJSMixin:
+    """Represent the mixin for property sensor descriptions."""
+
+    on_states: tuple[str, ...]
+
+
+@dataclass
+class PropertyZWaveJSEntityDescription(
+    BinarySensorEntityDescription, PropertyZWaveJSMixin
+):
+    """Represent the entity description for property name sensors."""
 
 
 # Mappings for Notification sensors
 # https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/notifications.json
-NOTIFICATION_SENSOR_MAPPINGS: list[NotificationSensorMapping] = [
-    {
+NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = (
+    NotificationZWaveJSEntityDescription(
         # NotificationType 1: Smoke Alarm - State Id's 1 and 2 - Smoke detected
-        "type": NOTIFICATION_SMOKE_ALARM,
-        "states": ["1", "2"],
-        "device_class": DEVICE_CLASS_SMOKE,
-    },
-    {
+        key=NOTIFICATION_SMOKE_ALARM,
+        states=("1", "2"),
+        device_class=DEVICE_CLASS_SMOKE,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 1: Smoke Alarm - All other State Id's
-        "type": NOTIFICATION_SMOKE_ALARM,
-        "device_class": DEVICE_CLASS_PROBLEM,
-    },
-    {
+        key=NOTIFICATION_SMOKE_ALARM,
+        device_class=DEVICE_CLASS_PROBLEM,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 2: Carbon Monoxide - State Id's 1 and 2
-        "type": NOTIFICATION_CARBON_MONOOXIDE,
-        "states": ["1", "2"],
-        "device_class": DEVICE_CLASS_GAS,
-    },
-    {
+        key=NOTIFICATION_CARBON_MONOOXIDE,
+        states=("1", "2"),
+        device_class=DEVICE_CLASS_GAS,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 2: Carbon Monoxide - All other State Id's
-        "type": NOTIFICATION_CARBON_MONOOXIDE,
-        "device_class": DEVICE_CLASS_PROBLEM,
-    },
-    {
+        key=NOTIFICATION_CARBON_MONOOXIDE,
+        device_class=DEVICE_CLASS_PROBLEM,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 3: Carbon Dioxide - State Id's 1 and 2
-        "type": NOTIFICATION_CARBON_DIOXIDE,
-        "states": ["1", "2"],
-        "device_class": DEVICE_CLASS_GAS,
-    },
-    {
+        key=NOTIFICATION_CARBON_DIOXIDE,
+        states=("1", "2"),
+        device_class=DEVICE_CLASS_GAS,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 3: Carbon Dioxide - All other State Id's
-        "type": NOTIFICATION_CARBON_DIOXIDE,
-        "device_class": DEVICE_CLASS_PROBLEM,
-    },
-    {
+        key=NOTIFICATION_CARBON_DIOXIDE,
+        device_class=DEVICE_CLASS_PROBLEM,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 4: Heat - State Id's 1, 2, 5, 6 (heat/underheat)
-        "type": NOTIFICATION_HEAT,
-        "states": ["1", "2", "5", "6"],
-        "device_class": DEVICE_CLASS_HEAT,
-    },
-    {
+        key=NOTIFICATION_HEAT,
+        states=("1", "2", "5", "6"),
+        device_class=DEVICE_CLASS_HEAT,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 4: Heat - All other State Id's
-        "type": NOTIFICATION_HEAT,
-        "device_class": DEVICE_CLASS_PROBLEM,
-    },
-    {
+        key=NOTIFICATION_HEAT,
+        device_class=DEVICE_CLASS_PROBLEM,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 5: Water - State Id's 1, 2, 3, 4
-        "type": NOTIFICATION_WATER,
-        "states": ["1", "2", "3", "4"],
-        "device_class": DEVICE_CLASS_MOISTURE,
-    },
-    {
+        key=NOTIFICATION_WATER,
+        states=("1", "2", "3", "4"),
+        device_class=DEVICE_CLASS_MOISTURE,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 5: Water - All other State Id's
-        "type": NOTIFICATION_WATER,
-        "device_class": DEVICE_CLASS_PROBLEM,
-    },
-    {
+        key=NOTIFICATION_WATER,
+        device_class=DEVICE_CLASS_PROBLEM,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 6: Access Control - State Id's 1, 2, 3, 4 (Lock)
-        "type": NOTIFICATION_ACCESS_CONTROL,
-        "states": ["1", "2", "3", "4"],
-        "device_class": DEVICE_CLASS_LOCK,
-    },
-    {
+        key=NOTIFICATION_ACCESS_CONTROL,
+        states=("1", "2", "3", "4"),
+        device_class=DEVICE_CLASS_LOCK,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 6: Access Control - State Id 16 (door/window open)
-        "type": NOTIFICATION_ACCESS_CONTROL,
-        "states": ["22"],
-        "device_class": DEVICE_CLASS_DOOR,
-    },
-    {
+        key=NOTIFICATION_ACCESS_CONTROL,
+        states=("22",),
+        device_class=DEVICE_CLASS_DOOR,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 6: Access Control - State Id 17 (door/window closed)
-        "type": NOTIFICATION_ACCESS_CONTROL,
-        "states": ["23"],
-        "enabled": False,
-    },
-    {
+        key=NOTIFICATION_ACCESS_CONTROL,
+        states=("23",),
+        entity_registry_enabled_default=False,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 7: Home Security - State Id's 1, 2 (intrusion)
-        "type": NOTIFICATION_HOME_SECURITY,
-        "states": ["1", "2"],
-        "device_class": DEVICE_CLASS_SAFETY,
-    },
-    {
+        key=NOTIFICATION_HOME_SECURITY,
+        states=("1", "2"),
+        device_class=DEVICE_CLASS_SAFETY,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 7: Home Security - State Id's 3, 4, 9 (tampering)
-        "type": NOTIFICATION_HOME_SECURITY,
-        "states": ["3", "4", "9"],
-        "device_class": DEVICE_CLASS_SAFETY,
-    },
-    {
+        key=NOTIFICATION_HOME_SECURITY,
+        states=("3", "4", "9"),
+        device_class=DEVICE_CLASS_SAFETY,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 7: Home Security - State Id's 5, 6 (glass breakage)
-        "type": NOTIFICATION_HOME_SECURITY,
-        "states": ["5", "6"],
-        "device_class": DEVICE_CLASS_SAFETY,
-    },
-    {
+        key=NOTIFICATION_HOME_SECURITY,
+        states=("5", "6"),
+        device_class=DEVICE_CLASS_SAFETY,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 7: Home Security - State Id's 7, 8 (motion)
-        "type": NOTIFICATION_HOME_SECURITY,
-        "states": ["7", "8"],
-        "device_class": DEVICE_CLASS_MOTION,
-    },
-    {
+        key=NOTIFICATION_HOME_SECURITY,
+        states=("7", "8"),
+        device_class=DEVICE_CLASS_MOTION,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 9: System - State Id's 1, 2, 6, 7
-        "type": NOTIFICATION_SYSTEM,
-        "states": ["1", "2", "6", "7"],
-        "device_class": DEVICE_CLASS_PROBLEM,
-    },
-    {
+        key=NOTIFICATION_SYSTEM,
+        states=("1", "2", "6", "7"),
+        device_class=DEVICE_CLASS_PROBLEM,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 10: Emergency - State Id's 1, 2, 3
-        "type": NOTIFICATION_EMERGENCY,
-        "states": ["1", "2", "3"],
-        "device_class": DEVICE_CLASS_PROBLEM,
-    },
-    {
+        key=NOTIFICATION_EMERGENCY,
+        states=("1", "2", "3"),
+        device_class=DEVICE_CLASS_PROBLEM,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 14: Siren
-        "type": NOTIFICATION_SIREN,
-        "states": ["1"],
-        "device_class": DEVICE_CLASS_SOUND,
-    },
-    {
+        key=NOTIFICATION_SIREN,
+        states=("1",),
+        device_class=DEVICE_CLASS_SOUND,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 18: Gas
-        "type": NOTIFICATION_GAS,
-        "states": ["1", "2", "3", "4"],
-        "device_class": DEVICE_CLASS_GAS,
-    },
-    {
+        key=NOTIFICATION_GAS,
+        states=("1", "2", "3", "4"),
+        device_class=DEVICE_CLASS_GAS,
+    ),
+    NotificationZWaveJSEntityDescription(
         # NotificationType 18: Gas
-        "type": NOTIFICATION_GAS,
-        "states": ["6"],
-        "device_class": DEVICE_CLASS_PROBLEM,
-    },
-]
-
-
-class PropertySensorMapping(TypedDict, total=False):
-    """Represent a property sensor mapping dict type."""
-
-    property_name: str  # required
-    on_states: list[str]  # required
-    device_class: str
-    enabled: bool
+        key=NOTIFICATION_GAS,
+        states=("6",),
+        device_class=DEVICE_CLASS_PROBLEM,
+    ),
+)
 
 
 # Mappings for property sensors
-PROPERTY_SENSOR_MAPPINGS: list[PropertySensorMapping] = [
-    {
-        "property_name": DOOR_STATUS_PROPERTY,
-        "on_states": ["open"],
-        "device_class": DEVICE_CLASS_DOOR,
-        "enabled": True,
-    },
-]
+PROPERTY_SENSOR_MAPPINGS: dict[str, PropertyZWaveJSEntityDescription] = {
+    DOOR_STATUS_PROPERTY: PropertyZWaveJSEntityDescription(
+        key=DOOR_STATUS_PROPERTY,
+        on_states=("open",),
+        device_class=DEVICE_CLASS_DOOR,
+    ),
+}
+
+
+# Mappings for boolean sensors
+BOOLEAN_SENSOR_MAPPINGS: dict[str, BinarySensorEntityDescription] = {
+    CommandClass.BATTERY: BinarySensorEntityDescription(
+        key=str(CommandClass.BATTERY),
+        device_class=DEVICE_CLASS_BATTERY,
+    ),
+}
 
 
 async def async_setup_entry(
@@ -242,8 +254,14 @@ async def async_setup_entry(
                 entities.append(
                     ZWaveNotificationBinarySensor(config_entry, client, info, state_key)
                 )
-        elif info.platform_hint == "property":
-            entities.append(ZWavePropertyBinarySensor(config_entry, client, info))
+        elif info.platform_hint == "property" and (
+            description := PROPERTY_SENSOR_MAPPINGS.get(
+                info.primary_value.property_name
+            )
+        ):
+            entities.append(
+                ZWavePropertyBinarySensor(config_entry, client, info, description)
+            )
         else:
             # boolean sensor
             entities.append(ZWaveBooleanBinarySensor(config_entry, client, info))
@@ -273,11 +291,10 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
 
         # Entity class attributes
         self._attr_name = self.generate_name(include_value_name=True)
-        self._attr_device_class = (
-            DEVICE_CLASS_BATTERY
-            if self.info.primary_value.command_class == CommandClass.BATTERY
-            else None
-        )
+        if description := BOOLEAN_SENSOR_MAPPINGS.get(
+            self.info.primary_value.command_class
+        ):
+            self.entity_description = description
 
     @property
     def is_on(self) -> bool | None:
@@ -301,7 +318,8 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
         super().__init__(config_entry, client, info)
         self.state_key = state_key
         # check if we have a custom mapping for this value
-        self._mapping_info = self._get_sensor_mapping()
+        if description := self._get_sensor_description():
+            self.entity_description = description
 
         # Entity class attributes
         self._attr_name = self.generate_name(
@@ -309,11 +327,7 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
             alternate_value_name=self.info.primary_value.property_name,
             additional_info=[self.info.primary_value.metadata.states[self.state_key]],
         )
-        self._attr_device_class = self._mapping_info.get("device_class")
         self._attr_unique_id = f"{self._attr_unique_id}.{self.state_key}"
-        self._attr_entity_registry_enabled_default = (
-            True if not self._mapping_info else self._mapping_info.get("enabled", True)
-        )
 
     @property
     def is_on(self) -> bool | None:
@@ -323,56 +337,39 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
         return int(self.info.primary_value.value) == int(self.state_key)
 
     @callback
-    def _get_sensor_mapping(self) -> NotificationSensorMapping:
+    def _get_sensor_description(self) -> NotificationZWaveJSEntityDescription | None:
         """Try to get a device specific mapping for this sensor."""
-        for mapping in NOTIFICATION_SENSOR_MAPPINGS:
+        for description in NOTIFICATION_SENSOR_MAPPINGS:
             if (
-                mapping["type"]
-                != self.info.primary_value.metadata.cc_specific[
+                int(description.key)
+                == self.info.primary_value.metadata.cc_specific[
                     CC_SPECIFIC_NOTIFICATION_TYPE
                 ]
-            ):
-                continue
-            if not mapping.get("states") or self.state_key in mapping["states"]:
-                # match found
-                return mapping
-        return {}
+            ) and (not description.states or self.state_key in description.states):
+                return description
+        return None
 
 
 class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
     """Representation of a Z-Wave binary_sensor from a property."""
 
+    entity_description: PropertyZWaveJSEntityDescription
+
     def __init__(
-        self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo
+        self,
+        config_entry: ConfigEntry,
+        client: ZwaveClient,
+        info: ZwaveDiscoveryInfo,
+        description: PropertyZWaveJSEntityDescription,
     ) -> None:
         """Initialize a ZWavePropertyBinarySensor entity."""
         super().__init__(config_entry, client, info)
-        # check if we have a custom mapping for this value
-        self._mapping_info = self._get_sensor_mapping()
-
-        # Entity class attributes
+        self.entity_description = description
         self._attr_name = self.generate_name(include_value_name=True)
-        self._attr_device_class = self._mapping_info.get("device_class")
-        # We hide some more advanced sensors by default to not overwhelm users
-        # unless explicitly stated in a mapping, assume deisabled by default
-        self._attr_entity_registry_enabled_default = self._mapping_info.get(
-            "enabled", False
-        )
 
     @property
     def is_on(self) -> bool | None:
         """Return if the sensor is on or off."""
         if self.info.primary_value.value is None:
             return None
-        return self.info.primary_value.value in self._mapping_info["on_states"]
-
-    @callback
-    def _get_sensor_mapping(self) -> PropertySensorMapping:
-        """Try to get a device specific mapping for this sensor."""
-        mapping_info = PropertySensorMapping()
-        for mapping in PROPERTY_SENSOR_MAPPINGS:
-            if mapping["property_name"] == self.info.primary_value.property_name:
-                mapping_info = mapping.copy()
-                break
-
-        return mapping_info
+        return self.info.primary_value.value in self.entity_description.on_states
diff --git a/tests/components/zwave_js/test_binary_sensor.py b/tests/components/zwave_js/test_binary_sensor.py
index 421c808bc0b..1cb91547a0a 100644
--- a/tests/components/zwave_js/test_binary_sensor.py
+++ b/tests/components/zwave_js/test_binary_sensor.py
@@ -1,7 +1,10 @@
 """Test the Z-Wave JS binary sensor platform."""
 from zwave_js_server.event import Event
 
-from homeassistant.components.binary_sensor import DEVICE_CLASS_MOTION
+from homeassistant.components.binary_sensor import (
+    DEVICE_CLASS_DOOR,
+    DEVICE_CLASS_MOTION,
+)
 from homeassistant.const import DEVICE_CLASS_BATTERY, STATE_OFF, STATE_ON
 from homeassistant.helpers import entity_registry as er
 
@@ -93,8 +96,9 @@ async def test_property_sensor_door_status(hass, lock_august_pro, integration):
     node = lock_august_pro
 
     state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR)
-    assert state is not None
+    assert state
     assert state.state == STATE_OFF
+    assert state.attributes["device_class"] == DEVICE_CLASS_DOOR
 
     # open door
     event = Event(
@@ -116,6 +120,7 @@ async def test_property_sensor_door_status(hass, lock_august_pro, integration):
     )
     node.receive_event(event)
     state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR)
+    assert state
     assert state.state == STATE_ON
 
     # close door
@@ -138,4 +143,5 @@ async def test_property_sensor_door_status(hass, lock_august_pro, integration):
     )
     node.receive_event(event)
     state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR)
+    assert state
     assert state.state == STATE_OFF