Add zwave_js binary sensor descriptions (#58641)

This commit is contained in:
Martin Hjelmare 2021-10-29 06:28:02 +02:00 committed by GitHub
parent 7516db3600
commit 3a76d92e0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 199 additions and 196 deletions

View File

@ -1,8 +1,8 @@
"""Representation of Z-Wave binary sensors.""" """Representation of Z-Wave binary sensors."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
import logging import logging
from typing import TypedDict
from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.const import CommandClass from zwave_js_server.const import CommandClass
@ -25,6 +25,7 @@ from homeassistant.components.binary_sensor import (
DEVICE_CLASS_SOUND, DEVICE_CLASS_SOUND,
DOMAIN as BINARY_SENSOR_DOMAIN, DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorEntity, BinarySensorEntity,
BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -38,186 +39,197 @@ from .entity import ZWaveBaseEntity
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
NOTIFICATION_SMOKE_ALARM = 1 NOTIFICATION_SMOKE_ALARM = "1"
NOTIFICATION_CARBON_MONOOXIDE = 2 NOTIFICATION_CARBON_MONOOXIDE = "2"
NOTIFICATION_CARBON_DIOXIDE = 3 NOTIFICATION_CARBON_DIOXIDE = "3"
NOTIFICATION_HEAT = 4 NOTIFICATION_HEAT = "4"
NOTIFICATION_WATER = 5 NOTIFICATION_WATER = "5"
NOTIFICATION_ACCESS_CONTROL = 6 NOTIFICATION_ACCESS_CONTROL = "6"
NOTIFICATION_HOME_SECURITY = 7 NOTIFICATION_HOME_SECURITY = "7"
NOTIFICATION_POWER_MANAGEMENT = 8 NOTIFICATION_POWER_MANAGEMENT = "8"
NOTIFICATION_SYSTEM = 9 NOTIFICATION_SYSTEM = "9"
NOTIFICATION_EMERGENCY = 10 NOTIFICATION_EMERGENCY = "10"
NOTIFICATION_CLOCK = 11 NOTIFICATION_CLOCK = "11"
NOTIFICATION_APPLIANCE = 12 NOTIFICATION_APPLIANCE = "12"
NOTIFICATION_HOME_HEALTH = 13 NOTIFICATION_HOME_HEALTH = "13"
NOTIFICATION_SIREN = 14 NOTIFICATION_SIREN = "14"
NOTIFICATION_WATER_VALVE = 15 NOTIFICATION_WATER_VALVE = "15"
NOTIFICATION_WEATHER = 16 NOTIFICATION_WEATHER = "16"
NOTIFICATION_IRRIGATION = 17 NOTIFICATION_IRRIGATION = "17"
NOTIFICATION_GAS = 18 NOTIFICATION_GAS = "18"
class NotificationSensorMapping(TypedDict, total=False): @dataclass
"""Represent a notification sensor mapping dict type.""" class NotificationZWaveJSEntityDescription(BinarySensorEntityDescription):
"""Represent a Z-Wave JS binary sensor entity description."""
type: int # required states: tuple[str, ...] | None = None
states: list[str]
device_class: str
enabled: bool @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 # Mappings for Notification sensors
# https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/notifications.json # 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 # NotificationType 1: Smoke Alarm - State Id's 1 and 2 - Smoke detected
"type": NOTIFICATION_SMOKE_ALARM, key=NOTIFICATION_SMOKE_ALARM,
"states": ["1", "2"], states=("1", "2"),
"device_class": DEVICE_CLASS_SMOKE, device_class=DEVICE_CLASS_SMOKE,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 1: Smoke Alarm - All other State Id's # NotificationType 1: Smoke Alarm - All other State Id's
"type": NOTIFICATION_SMOKE_ALARM, key=NOTIFICATION_SMOKE_ALARM,
"device_class": DEVICE_CLASS_PROBLEM, device_class=DEVICE_CLASS_PROBLEM,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 2: Carbon Monoxide - State Id's 1 and 2 # NotificationType 2: Carbon Monoxide - State Id's 1 and 2
"type": NOTIFICATION_CARBON_MONOOXIDE, key=NOTIFICATION_CARBON_MONOOXIDE,
"states": ["1", "2"], states=("1", "2"),
"device_class": DEVICE_CLASS_GAS, device_class=DEVICE_CLASS_GAS,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 2: Carbon Monoxide - All other State Id's # NotificationType 2: Carbon Monoxide - All other State Id's
"type": NOTIFICATION_CARBON_MONOOXIDE, key=NOTIFICATION_CARBON_MONOOXIDE,
"device_class": DEVICE_CLASS_PROBLEM, device_class=DEVICE_CLASS_PROBLEM,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 3: Carbon Dioxide - State Id's 1 and 2 # NotificationType 3: Carbon Dioxide - State Id's 1 and 2
"type": NOTIFICATION_CARBON_DIOXIDE, key=NOTIFICATION_CARBON_DIOXIDE,
"states": ["1", "2"], states=("1", "2"),
"device_class": DEVICE_CLASS_GAS, device_class=DEVICE_CLASS_GAS,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 3: Carbon Dioxide - All other State Id's # NotificationType 3: Carbon Dioxide - All other State Id's
"type": NOTIFICATION_CARBON_DIOXIDE, key=NOTIFICATION_CARBON_DIOXIDE,
"device_class": DEVICE_CLASS_PROBLEM, device_class=DEVICE_CLASS_PROBLEM,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 4: Heat - State Id's 1, 2, 5, 6 (heat/underheat) # NotificationType 4: Heat - State Id's 1, 2, 5, 6 (heat/underheat)
"type": NOTIFICATION_HEAT, key=NOTIFICATION_HEAT,
"states": ["1", "2", "5", "6"], states=("1", "2", "5", "6"),
"device_class": DEVICE_CLASS_HEAT, device_class=DEVICE_CLASS_HEAT,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 4: Heat - All other State Id's # NotificationType 4: Heat - All other State Id's
"type": NOTIFICATION_HEAT, key=NOTIFICATION_HEAT,
"device_class": DEVICE_CLASS_PROBLEM, device_class=DEVICE_CLASS_PROBLEM,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 5: Water - State Id's 1, 2, 3, 4 # NotificationType 5: Water - State Id's 1, 2, 3, 4
"type": NOTIFICATION_WATER, key=NOTIFICATION_WATER,
"states": ["1", "2", "3", "4"], states=("1", "2", "3", "4"),
"device_class": DEVICE_CLASS_MOISTURE, device_class=DEVICE_CLASS_MOISTURE,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 5: Water - All other State Id's # NotificationType 5: Water - All other State Id's
"type": NOTIFICATION_WATER, key=NOTIFICATION_WATER,
"device_class": DEVICE_CLASS_PROBLEM, device_class=DEVICE_CLASS_PROBLEM,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 6: Access Control - State Id's 1, 2, 3, 4 (Lock) # NotificationType 6: Access Control - State Id's 1, 2, 3, 4 (Lock)
"type": NOTIFICATION_ACCESS_CONTROL, key=NOTIFICATION_ACCESS_CONTROL,
"states": ["1", "2", "3", "4"], states=("1", "2", "3", "4"),
"device_class": DEVICE_CLASS_LOCK, device_class=DEVICE_CLASS_LOCK,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 6: Access Control - State Id 16 (door/window open) # NotificationType 6: Access Control - State Id 16 (door/window open)
"type": NOTIFICATION_ACCESS_CONTROL, key=NOTIFICATION_ACCESS_CONTROL,
"states": ["22"], states=("22",),
"device_class": DEVICE_CLASS_DOOR, device_class=DEVICE_CLASS_DOOR,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 6: Access Control - State Id 17 (door/window closed) # NotificationType 6: Access Control - State Id 17 (door/window closed)
"type": NOTIFICATION_ACCESS_CONTROL, key=NOTIFICATION_ACCESS_CONTROL,
"states": ["23"], states=("23",),
"enabled": False, entity_registry_enabled_default=False,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 7: Home Security - State Id's 1, 2 (intrusion) # NotificationType 7: Home Security - State Id's 1, 2 (intrusion)
"type": NOTIFICATION_HOME_SECURITY, key=NOTIFICATION_HOME_SECURITY,
"states": ["1", "2"], states=("1", "2"),
"device_class": DEVICE_CLASS_SAFETY, device_class=DEVICE_CLASS_SAFETY,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 7: Home Security - State Id's 3, 4, 9 (tampering) # NotificationType 7: Home Security - State Id's 3, 4, 9 (tampering)
"type": NOTIFICATION_HOME_SECURITY, key=NOTIFICATION_HOME_SECURITY,
"states": ["3", "4", "9"], states=("3", "4", "9"),
"device_class": DEVICE_CLASS_SAFETY, device_class=DEVICE_CLASS_SAFETY,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 7: Home Security - State Id's 5, 6 (glass breakage) # NotificationType 7: Home Security - State Id's 5, 6 (glass breakage)
"type": NOTIFICATION_HOME_SECURITY, key=NOTIFICATION_HOME_SECURITY,
"states": ["5", "6"], states=("5", "6"),
"device_class": DEVICE_CLASS_SAFETY, device_class=DEVICE_CLASS_SAFETY,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 7: Home Security - State Id's 7, 8 (motion) # NotificationType 7: Home Security - State Id's 7, 8 (motion)
"type": NOTIFICATION_HOME_SECURITY, key=NOTIFICATION_HOME_SECURITY,
"states": ["7", "8"], states=("7", "8"),
"device_class": DEVICE_CLASS_MOTION, device_class=DEVICE_CLASS_MOTION,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 9: System - State Id's 1, 2, 6, 7 # NotificationType 9: System - State Id's 1, 2, 6, 7
"type": NOTIFICATION_SYSTEM, key=NOTIFICATION_SYSTEM,
"states": ["1", "2", "6", "7"], states=("1", "2", "6", "7"),
"device_class": DEVICE_CLASS_PROBLEM, device_class=DEVICE_CLASS_PROBLEM,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 10: Emergency - State Id's 1, 2, 3 # NotificationType 10: Emergency - State Id's 1, 2, 3
"type": NOTIFICATION_EMERGENCY, key=NOTIFICATION_EMERGENCY,
"states": ["1", "2", "3"], states=("1", "2", "3"),
"device_class": DEVICE_CLASS_PROBLEM, device_class=DEVICE_CLASS_PROBLEM,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 14: Siren # NotificationType 14: Siren
"type": NOTIFICATION_SIREN, key=NOTIFICATION_SIREN,
"states": ["1"], states=("1",),
"device_class": DEVICE_CLASS_SOUND, device_class=DEVICE_CLASS_SOUND,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 18: Gas # NotificationType 18: Gas
"type": NOTIFICATION_GAS, key=NOTIFICATION_GAS,
"states": ["1", "2", "3", "4"], states=("1", "2", "3", "4"),
"device_class": DEVICE_CLASS_GAS, device_class=DEVICE_CLASS_GAS,
}, ),
{ NotificationZWaveJSEntityDescription(
# NotificationType 18: Gas # NotificationType 18: Gas
"type": NOTIFICATION_GAS, key=NOTIFICATION_GAS,
"states": ["6"], states=("6",),
"device_class": DEVICE_CLASS_PROBLEM, 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
# Mappings for property sensors # Mappings for property sensors
PROPERTY_SENSOR_MAPPINGS: list[PropertySensorMapping] = [ PROPERTY_SENSOR_MAPPINGS: dict[str, PropertyZWaveJSEntityDescription] = {
{ DOOR_STATUS_PROPERTY: PropertyZWaveJSEntityDescription(
"property_name": DOOR_STATUS_PROPERTY, key=DOOR_STATUS_PROPERTY,
"on_states": ["open"], on_states=("open",),
"device_class": DEVICE_CLASS_DOOR, device_class=DEVICE_CLASS_DOOR,
"enabled": True, ),
}, }
]
# 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( async def async_setup_entry(
@ -242,8 +254,14 @@ async def async_setup_entry(
entities.append( entities.append(
ZWaveNotificationBinarySensor(config_entry, client, info, state_key) ZWaveNotificationBinarySensor(config_entry, client, info, state_key)
) )
elif info.platform_hint == "property": elif info.platform_hint == "property" and (
entities.append(ZWavePropertyBinarySensor(config_entry, client, info)) description := PROPERTY_SENSOR_MAPPINGS.get(
info.primary_value.property_name
)
):
entities.append(
ZWavePropertyBinarySensor(config_entry, client, info, description)
)
else: else:
# boolean sensor # boolean sensor
entities.append(ZWaveBooleanBinarySensor(config_entry, client, info)) entities.append(ZWaveBooleanBinarySensor(config_entry, client, info))
@ -273,11 +291,10 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
# Entity class attributes # Entity class attributes
self._attr_name = self.generate_name(include_value_name=True) self._attr_name = self.generate_name(include_value_name=True)
self._attr_device_class = ( if description := BOOLEAN_SENSOR_MAPPINGS.get(
DEVICE_CLASS_BATTERY self.info.primary_value.command_class
if self.info.primary_value.command_class == CommandClass.BATTERY ):
else None self.entity_description = description
)
@property @property
def is_on(self) -> bool | None: def is_on(self) -> bool | None:
@ -301,7 +318,8 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
super().__init__(config_entry, client, info) super().__init__(config_entry, client, info)
self.state_key = state_key self.state_key = state_key
# check if we have a custom mapping for this value # 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 # Entity class attributes
self._attr_name = self.generate_name( self._attr_name = self.generate_name(
@ -309,11 +327,7 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
alternate_value_name=self.info.primary_value.property_name, alternate_value_name=self.info.primary_value.property_name,
additional_info=[self.info.primary_value.metadata.states[self.state_key]], 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_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 @property
def is_on(self) -> bool | None: 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) return int(self.info.primary_value.value) == int(self.state_key)
@callback @callback
def _get_sensor_mapping(self) -> NotificationSensorMapping: def _get_sensor_description(self) -> NotificationZWaveJSEntityDescription | None:
"""Try to get a device specific mapping for this sensor.""" """Try to get a device specific mapping for this sensor."""
for mapping in NOTIFICATION_SENSOR_MAPPINGS: for description in NOTIFICATION_SENSOR_MAPPINGS:
if ( if (
mapping["type"] int(description.key)
!= self.info.primary_value.metadata.cc_specific[ == self.info.primary_value.metadata.cc_specific[
CC_SPECIFIC_NOTIFICATION_TYPE CC_SPECIFIC_NOTIFICATION_TYPE
] ]
): ) and (not description.states or self.state_key in description.states):
continue return description
if not mapping.get("states") or self.state_key in mapping["states"]: return None
# match found
return mapping
return {}
class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity): class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
"""Representation of a Z-Wave binary_sensor from a property.""" """Representation of a Z-Wave binary_sensor from a property."""
entity_description: PropertyZWaveJSEntityDescription
def __init__( def __init__(
self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo self,
config_entry: ConfigEntry,
client: ZwaveClient,
info: ZwaveDiscoveryInfo,
description: PropertyZWaveJSEntityDescription,
) -> None: ) -> None:
"""Initialize a ZWavePropertyBinarySensor entity.""" """Initialize a ZWavePropertyBinarySensor entity."""
super().__init__(config_entry, client, info) super().__init__(config_entry, client, info)
# check if we have a custom mapping for this value self.entity_description = description
self._mapping_info = self._get_sensor_mapping()
# Entity class attributes
self._attr_name = self.generate_name(include_value_name=True) 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 @property
def is_on(self) -> bool | None: def is_on(self) -> bool | None:
"""Return if the sensor is on or off.""" """Return if the sensor is on or off."""
if self.info.primary_value.value is None: if self.info.primary_value.value is None:
return None return None
return self.info.primary_value.value in self._mapping_info["on_states"] return self.info.primary_value.value in self.entity_description.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

View File

@ -1,7 +1,10 @@
"""Test the Z-Wave JS binary sensor platform.""" """Test the Z-Wave JS binary sensor platform."""
from zwave_js_server.event import Event 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.const import DEVICE_CLASS_BATTERY, STATE_OFF, STATE_ON
from homeassistant.helpers import entity_registry as er 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 node = lock_august_pro
state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR) state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR)
assert state is not None assert state
assert state.state == STATE_OFF assert state.state == STATE_OFF
assert state.attributes["device_class"] == DEVICE_CLASS_DOOR
# open door # open door
event = Event( event = Event(
@ -116,6 +120,7 @@ async def test_property_sensor_door_status(hass, lock_august_pro, integration):
) )
node.receive_event(event) node.receive_event(event)
state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR) state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR)
assert state
assert state.state == STATE_ON assert state.state == STATE_ON
# close door # close door
@ -138,4 +143,5 @@ async def test_property_sensor_door_status(hass, lock_august_pro, integration):
) )
node.receive_event(event) node.receive_event(event)
state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR) state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR)
assert state
assert state.state == STATE_OFF assert state.state == STATE_OFF