Add zwave_js binary sensor entity category (#58703)

* Add zwave_js binary sensor entity category

* Handle non idle notification state

* Fix door state

* Fix duplicate door state description

* Add tests
This commit is contained in:
Martin Hjelmare 2021-11-09 18:26:34 +01:00 committed by GitHub
parent 4b228e3add
commit d5fcf0b622
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 122 additions and 28 deletions

View File

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

View File

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

View File

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