diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index a5883e9bcbf..af094407359 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -19,15 +19,18 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_LOCK, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, + DEVICE_CLASS_PLUG, DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_TAMPER, DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -63,6 +66,7 @@ NOTIFICATION_GAS = "18" class NotificationZWaveJSEntityDescription(BinarySensorEntityDescription): """Represent a Z-Wave JS binary sensor entity description.""" + off_state: str = "0" states: tuple[str, ...] | None = None @@ -145,17 +149,12 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = device_class=DEVICE_CLASS_LOCK, ), NotificationZWaveJSEntityDescription( - # NotificationType 6: Access Control - State Id 16 (door/window open) + # NotificationType 6: Access Control - State Id 22 (door/window open) key=NOTIFICATION_ACCESS_CONTROL, - states=("22",), + off_state="23", + states=("22", "23"), device_class=DEVICE_CLASS_DOOR, ), - NotificationZWaveJSEntityDescription( - # NotificationType 6: Access Control - State Id 17 (door/window closed) - key=NOTIFICATION_ACCESS_CONTROL, - states=("23",), - entity_registry_enabled_default=False, - ), NotificationZWaveJSEntityDescription( # NotificationType 7: Home Security - State Id's 1, 2 (intrusion) key=NOTIFICATION_HOME_SECURITY, @@ -166,7 +165,8 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = # NotificationType 7: Home Security - State Id's 3, 4, 9 (tampering) key=NOTIFICATION_HOME_SECURITY, states=("3", "4", "9"), - device_class=DEVICE_CLASS_SAFETY, + device_class=DEVICE_CLASS_TAMPER, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), NotificationZWaveJSEntityDescription( # NotificationType 7: Home Security - State Id's 5, 6 (glass breakage) @@ -180,6 +180,23 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = states=("7", "8"), device_class=DEVICE_CLASS_MOTION, ), + NotificationZWaveJSEntityDescription( + # NotificationType 8: Power Management - + # State Id's 2, 3 (Mains status) + key=NOTIFICATION_POWER_MANAGEMENT, + off_state="2", + states=("2", "3"), + device_class=DEVICE_CLASS_PLUG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + NotificationZWaveJSEntityDescription( + # NotificationType 8: Power Management - + # State Id's 10, 11, 17 (Battery maintenance status) + key=NOTIFICATION_POWER_MANAGEMENT, + states=("10", "11", "17"), + device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), NotificationZWaveJSEntityDescription( # NotificationType 9: System - State Id's 1, 2, 6, 7 key=NOTIFICATION_SYSTEM, @@ -228,6 +245,7 @@ BOOLEAN_SENSOR_MAPPINGS: dict[str, BinarySensorEntityDescription] = { CommandClass.BATTERY: BinarySensorEntityDescription( key=str(CommandClass.BATTERY), device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } @@ -251,16 +269,41 @@ async def async_setup_entry( # ignore idle key (0) if state_key == "0": continue + + notification_description: NotificationZWaveJSEntityDescription | None = ( + None + ) + + for description in NOTIFICATION_SENSOR_MAPPINGS: + if ( + int(description.key) + == info.primary_value.metadata.cc_specific[ + CC_SPECIFIC_NOTIFICATION_TYPE + ] + ) and (not description.states or state_key in description.states): + notification_description = description + break + + if ( + notification_description + and notification_description.off_state == state_key + ): + continue + entities.append( - ZWaveNotificationBinarySensor(config_entry, client, info, state_key) + ZWaveNotificationBinarySensor( + config_entry, client, info, state_key, notification_description + ) ) elif info.platform_hint == "property" and ( - description := PROPERTY_SENSOR_MAPPINGS.get( + property_description := PROPERTY_SENSOR_MAPPINGS.get( info.primary_value.property_name ) ): entities.append( - ZWavePropertyBinarySensor(config_entry, client, info, description) + ZWavePropertyBinarySensor( + config_entry, client, info, property_description + ) ) else: # boolean sensor @@ -313,12 +356,12 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): client: ZwaveClient, info: ZwaveDiscoveryInfo, state_key: str, + description: NotificationZWaveJSEntityDescription | None = None, ) -> None: """Initialize a ZWaveNotificationBinarySensor entity.""" super().__init__(config_entry, client, info) self.state_key = state_key - # check if we have a custom mapping for this value - if description := self._get_sensor_description(): + if description: self.entity_description = description # Entity class attributes @@ -336,19 +379,6 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): return None return int(self.info.primary_value.value) == int(self.state_key) - @callback - def _get_sensor_description(self) -> NotificationZWaveJSEntityDescription | None: - """Try to get a device specific mapping for this sensor.""" - for description in NOTIFICATION_SENSOR_MAPPINGS: - if ( - int(description.key) - == self.info.primary_value.metadata.cc_specific[ - CC_SPECIFIC_NOTIFICATION_TYPE - ] - ) 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.""" diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index c16ab00b2eb..d73daacdc75 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -1,6 +1,9 @@ """Provide common test tools for Z-Wave JS.""" AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" BATTERY_SENSOR = "sensor.multisensor_6_battery_level" +TAMPER_SENSOR = ( + "binary_sensor.multisensor_6_home_security_tampering_product_cover_removed" +) HUMIDITY_SENSOR = "sensor.multisensor_6_humidity" POWER_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2" diff --git a/tests/components/zwave_js/test_binary_sensor.py b/tests/components/zwave_js/test_binary_sensor.py index 1cb91547a0a..d5aab6cd0f9 100644 --- a/tests/components/zwave_js/test_binary_sensor.py +++ b/tests/components/zwave_js/test_binary_sensor.py @@ -1,11 +1,19 @@ """Test the Z-Wave JS binary sensor platform.""" from zwave_js_server.event import Event +from zwave_js_server.model.node import Node from homeassistant.components.binary_sensor import ( DEVICE_CLASS_DOOR, DEVICE_CLASS_MOTION, + DEVICE_CLASS_TAMPER, ) -from homeassistant.const import DEVICE_CLASS_BATTERY, STATE_OFF, STATE_ON +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from .common import ( @@ -14,8 +22,11 @@ from .common import ( LOW_BATTERY_BINARY_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR, PROPERTY_DOOR_STATUS_BINARY_SENSOR, + TAMPER_SENSOR, ) +from tests.common import MockConfigEntry + async def test_low_battery_sensor(hass, multisensor_6, integration): """Test boolean binary sensor of type low battery.""" @@ -25,6 +36,12 @@ async def test_low_battery_sensor(hass, multisensor_6, integration): assert state.state == STATE_OFF assert state.attributes["device_class"] == DEVICE_CLASS_BATTERY + registry = er.async_get(hass) + entity_entry = registry.async_get(LOW_BATTERY_BINARY_SENSOR) + + assert entity_entry + assert entity_entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + async def test_enabled_legacy_sensor(hass, ecolink_door_sensor, integration): """Test enabled legacy boolean binary sensor.""" @@ -90,6 +107,50 @@ async def test_notification_sensor(hass, multisensor_6, integration): assert state.state == STATE_ON assert state.attributes["device_class"] == DEVICE_CLASS_MOTION + state = hass.states.get(TAMPER_SENSOR) + + assert state + assert state.state == STATE_OFF + assert state.attributes["device_class"] == DEVICE_CLASS_TAMPER + + registry = er.async_get(hass) + entity_entry = registry.async_get(TAMPER_SENSOR) + + assert entity_entry + assert entity_entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + + +async def test_notification_off_state( + hass: HomeAssistant, + lock_popp_electric_strike_lock_control: Node, +): + """Test the description off_state attribute of certain notification sensors.""" + node = lock_popp_electric_strike_lock_control + # Remove all other values except the door state value. + node.values = { + value_id: value + for value_id, value in node.values.items() + if value_id == "62-113-0-Access Control-Door state" + } + + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + door_states = [ + state + for state in hass.states.async_all("binary_sensor") + if state.attributes.get("device_class") == DEVICE_CLASS_DOOR + ] + + # Only one entity should be created for the Door state notification states. + assert len(door_states) == 1 + + state = door_states[0] + assert state + assert state.entity_id == "binary_sensor.node_62_access_control_window_door_is_open" + async def test_property_sensor_door_status(hass, lock_august_pro, integration): """Test property binary sensor with sensor mapping (doorStatus)."""