Add binary_sensor platform to zwave_js (#45081)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Marcel van der Veldt 2021-01-15 15:15:03 +01:00 committed by GitHub
parent e1427c45f2
commit 071c8cc67d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 811 additions and 1 deletions

View File

@ -0,0 +1,354 @@
"""Representation of Z-Wave binary sensors."""
import logging
from typing import Callable, List, Optional, TypedDict
from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.const import CommandClass
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_DOOR,
DEVICE_CLASS_GAS,
DEVICE_CLASS_HEAT,
DEVICE_CLASS_LOCK,
DEVICE_CLASS_MOISTURE,
DEVICE_CLASS_MOTION,
DEVICE_CLASS_POWER,
DEVICE_CLASS_PROBLEM,
DEVICE_CLASS_SAFETY,
DEVICE_CLASS_SMOKE,
DEVICE_CLASS_SOUND,
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN
from .discovery import ZwaveDiscoveryInfo
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
class NotificationSensorMapping(TypedDict, total=False):
"""Represent a notification sensor mapping dict type."""
type: int # required
states: List[int] # required
device_class: str
enabled: bool
# Mappings for Notification sensors
NOTIFICATION_SENSOR_MAPPINGS: List[NotificationSensorMapping] = [
{
# NotificationType 1: Smoke Alarm - State Id's 1 and 2
# Assuming here that Value 1 and 2 are not present at the same time
"type": NOTIFICATION_SMOKE_ALARM,
"states": [1, 2],
"device_class": DEVICE_CLASS_SMOKE,
},
{
# NotificationType 1: Smoke Alarm - All other State Id's
# Create as disabled sensors
"type": NOTIFICATION_SMOKE_ALARM,
"states": [3, 4, 5, 6, 7, 8],
"device_class": DEVICE_CLASS_SMOKE,
"enabled": False,
},
{
# NotificationType 2: Carbon Monoxide - State Id's 1 and 2
"type": NOTIFICATION_CARBON_MONOOXIDE,
"states": [1, 2],
"device_class": DEVICE_CLASS_GAS,
},
{
# NotificationType 2: Carbon Monoxide - All other State Id's
"type": NOTIFICATION_CARBON_MONOOXIDE,
"states": [4, 5, 7],
"device_class": DEVICE_CLASS_GAS,
"enabled": False,
},
{
# NotificationType 3: Carbon Dioxide - State Id's 1 and 2
"type": NOTIFICATION_CARBON_DIOXIDE,
"states": [1, 2],
"device_class": DEVICE_CLASS_GAS,
},
{
# NotificationType 3: Carbon Dioxide - All other State Id's
"type": NOTIFICATION_CARBON_DIOXIDE,
"states": [4, 5, 7],
"device_class": DEVICE_CLASS_GAS,
"enabled": False,
},
{
# 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,
},
{
# NotificationType 4: Heat - All other State Id's
"type": NOTIFICATION_HEAT,
"states": [3, 4, 8, 10, 11],
"device_class": DEVICE_CLASS_HEAT,
"enabled": False,
},
{
# NotificationType 5: Water - State Id's 1, 2, 3, 4
"type": NOTIFICATION_WATER,
"states": [1, 2, 3, 4],
"device_class": DEVICE_CLASS_MOISTURE,
},
{
# NotificationType 5: Water - All other State Id's
"type": NOTIFICATION_WATER,
"states": [5],
"device_class": DEVICE_CLASS_MOISTURE,
"enabled": False,
},
{
# 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,
},
{
# NotificationType 6: Access Control - State Id 22 (door/window open)
"type": NOTIFICATION_ACCESS_CONTROL,
"states": [22],
"device_class": DEVICE_CLASS_DOOR,
},
{
# NotificationType 7: Home Security - State Id's 1, 2 (intrusion)
# Assuming that value 1 and 2 are not present at the same time
"type": NOTIFICATION_HOME_SECURITY,
"states": [1, 2],
"device_class": DEVICE_CLASS_SAFETY,
},
{
# NotificationType 7: Home Security - State Id's 3, 4, 9 (tampering)
"type": NOTIFICATION_HOME_SECURITY,
"states": [3, 4, 9],
"device_class": DEVICE_CLASS_SAFETY,
},
{
# NotificationType 7: Home Security - State Id's 5, 6 (glass breakage)
# Assuming that value 5 and 6 are not present at the same time
"type": NOTIFICATION_HOME_SECURITY,
"states": [5, 6],
"device_class": DEVICE_CLASS_SAFETY,
},
{
# NotificationType 7: Home Security - State Id's 7, 8 (motion)
"type": NOTIFICATION_HOME_SECURITY,
"states": [7, 8],
"device_class": DEVICE_CLASS_MOTION,
},
{
# NotificationType 8: Power management - Values 1...9
"type": NOTIFICATION_POWER_MANAGEMENT,
"states": [1, 2, 3, 4, 5, 6, 7, 8, 9],
"device_class": DEVICE_CLASS_POWER,
"enabled": False,
},
{
# NotificationType 8: Power management - Values 10...15
# Battery values (mutually exclusive)
"type": NOTIFICATION_POWER_MANAGEMENT,
"states": [10, 11, 12, 13, 14, 15],
"device_class": DEVICE_CLASS_BATTERY,
"enabled": False,
},
{
# NotificationType 9: System - State Id's 1, 2, 6, 7
"type": NOTIFICATION_SYSTEM,
"states": [1, 2, 6, 7],
"device_class": DEVICE_CLASS_PROBLEM,
"enabled": False,
},
{
# NotificationType 10: Emergency - State Id's 1, 2, 3
"type": NOTIFICATION_EMERGENCY,
"states": [1, 2, 3],
"device_class": DEVICE_CLASS_PROBLEM,
},
{
# NotificationType 11: Clock - State Id's 1, 2
"type": NOTIFICATION_CLOCK,
"states": [1, 2],
"enabled": False,
},
{
# NotificationType 12: Appliance - All State Id's
"type": NOTIFICATION_APPLIANCE,
"states": list(range(1, 22)),
},
{
# NotificationType 13: Home Health - State Id's 1,2,3,4,5
"type": NOTIFICATION_APPLIANCE,
"states": [1, 2, 3, 4, 5],
},
{
# NotificationType 14: Siren
"type": NOTIFICATION_SIREN,
"states": [1],
"device_class": DEVICE_CLASS_SOUND,
},
{
# NotificationType 15: Water valve
# ignore non-boolean values
"type": NOTIFICATION_WATER_VALVE,
"states": [3, 4],
"device_class": DEVICE_CLASS_PROBLEM,
},
{
# NotificationType 16: Weather
"type": NOTIFICATION_WEATHER,
"states": [1, 2],
"device_class": DEVICE_CLASS_PROBLEM,
},
{
# NotificationType 17: Irrigation
# ignore non-boolean values
"type": NOTIFICATION_IRRIGATION,
"states": [1, 2, 3, 4, 5],
},
{
# NotificationType 18: Gas
"type": NOTIFICATION_GAS,
"states": [1, 2, 3, 4],
"device_class": DEVICE_CLASS_GAS,
},
{
# NotificationType 18: Gas
"type": NOTIFICATION_GAS,
"states": [6],
"device_class": DEVICE_CLASS_PROBLEM,
},
]
async def async_setup_entry(
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
) -> None:
"""Set up Z-Wave binary sensor from config entry."""
client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
@callback
def async_add_binary_sensor(info: ZwaveDiscoveryInfo) -> None:
"""Add Z-Wave Binary Sensor."""
entities: List[ZWaveBaseEntity] = []
if info.platform_hint == "notification":
entities.append(ZWaveNotificationBinarySensor(client, info))
else:
# boolean sensor
entities.append(ZWaveBooleanBinarySensor(client, info))
async_add_entities(entities)
hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append(
async_dispatcher_connect(
hass, f"{DOMAIN}_add_{BINARY_SENSOR_DOMAIN}", async_add_binary_sensor
)
)
class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
"""Representation of a Z-Wave binary_sensor."""
@property
def is_on(self) -> bool:
"""Return if the sensor is on or off."""
return bool(self.info.primary_value.value)
@property
def device_class(self) -> Optional[str]:
"""Return device class."""
if self.info.primary_value.command_class == CommandClass.BATTERY:
return DEVICE_CLASS_BATTERY
return None
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
if self.info.primary_value.command_class == CommandClass.SENSOR_BINARY:
# Legacy binary sensors are phased out (replaced by notification sensors)
# Disable by default to not confuse users
if self.info.node.device_class.generic != "Binary Sensor":
return False
return True
class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
"""Representation of a Z-Wave binary_sensor from Notification CommandClass."""
def __init__(self, client: ZwaveClient, info: ZwaveDiscoveryInfo) -> None:
"""Initialize a ZWaveNotificationBinarySensor entity."""
super().__init__(client, info)
# check if we have a custom mapping for this value
self._mapping_info = self._get_sensor_mapping()
@property
def is_on(self) -> bool:
"""Return if the sensor is on or off."""
if self._mapping_info:
return self.info.primary_value.value in self._mapping_info["states"]
return bool(self.info.primary_value.value != 0)
@property
def device_class(self) -> Optional[str]:
"""Return device class."""
return self._mapping_info.get("device_class")
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
# We hide some more advanced sensors by default to not overwhelm users
if not self._mapping_info:
# consider value for which we do not have a mapping as advanced.
return False
return self._mapping_info.get("enabled", True)
@callback
def _get_sensor_mapping(self) -> NotificationSensorMapping:
"""Try to get a device specific mapping for this sensor."""
for mapping in NOTIFICATION_SENSOR_MAPPINGS:
if mapping["type"] != int(
self.info.primary_value.metadata.cc_specific["notificationType"]
):
continue
for state_key in self.info.primary_value.metadata.states:
# make sure the key is int
state_key = int(state_key)
if state_key not in mapping["states"]:
continue
# match found
mapping_info = mapping.copy()
return mapping_info
return {}

View File

@ -3,7 +3,7 @@
DOMAIN = "zwave_js"
NAME = "Z-Wave JS"
PLATFORMS = ["light", "sensor", "switch"]
PLATFORMS = ["binary_sensor", "light", "sensor", "switch"]
DATA_CLIENT = "client"
DATA_UNSUBSCRIBE = "unsubs"

View File

@ -72,6 +72,24 @@ DISCOVERY_SCHEMAS = [
property={"currentValue"},
type={"number"},
),
# binary sensors
ZWaveDiscoverySchema(
platform="binary_sensor",
hint="boolean",
command_class={
CommandClass.SENSOR_BINARY,
CommandClass.BATTERY,
},
type={"boolean"},
),
ZWaveDiscoverySchema(
platform="binary_sensor",
hint="notification",
command_class={
CommandClass.NOTIFICATION,
},
type={"number"},
),
# generic text sensors
ZWaveDiscoverySchema(
platform="sensor",

View File

@ -3,3 +3,7 @@ AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature"
ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2"
POWER_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports_current_value"
LOW_BATTERY_BINARY_SENSOR = "binary_sensor.multisensor_6_low_battery_level"
ENABLED_LEGACY_BINARY_SENSOR = "binary_sensor.z_wave_door_window_sensor_any"
DISABLED_LEGACY_BINARY_SENSOR = "binary_sensor.multisensor_6_any"
NOTIFICATION_MOTION_BINARY_SENSOR = "binary_sensor.multisensor_6_motion_sensor_status"

View File

@ -31,6 +31,12 @@ def multisensor_6_state_fixture():
return json.loads(load_fixture("zwave_js/multisensor_6_state.json"))
@pytest.fixture(name="ecolink_door_sensor_state", scope="session")
def ecolink_door_sensor_state_fixture():
"""Load the Ecolink Door/Window Sensor node state fixture data."""
return json.loads(load_fixture("zwave_js/ecolink_door_sensor_state.json"))
@pytest.fixture(name="hank_binary_switch_state", scope="session")
def binary_switch_state_fixture():
"""Load the hank binary switch node state fixture data."""
@ -62,6 +68,14 @@ def multisensor_6_fixture(client, multisensor_6_state):
return node
@pytest.fixture(name="ecolink_door_sensor")
def legacy_binary_sensor_fixture(client, ecolink_door_sensor_state):
"""Mock a legacy_binary_sensor node."""
node = Node(client, ecolink_door_sensor_state)
client.driver.controller.nodes[node.node_id] = node
return node
@pytest.fixture(name="hank_binary_switch")
def hank_binary_switch_fixture(client, hank_binary_switch_state):
"""Mock a binary switch node."""

View File

@ -0,0 +1,86 @@
"""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.const import DEVICE_CLASS_BATTERY, STATE_OFF, STATE_ON
from .common import (
DISABLED_LEGACY_BINARY_SENSOR,
ENABLED_LEGACY_BINARY_SENSOR,
LOW_BATTERY_BINARY_SENSOR,
NOTIFICATION_MOTION_BINARY_SENSOR,
)
async def test_low_battery_sensor(hass, multisensor_6, integration):
"""Test boolean binary sensor of type low battery."""
state = hass.states.get(LOW_BATTERY_BINARY_SENSOR)
assert state
assert state.state == STATE_OFF
assert state.attributes["device_class"] == DEVICE_CLASS_BATTERY
async def test_enabled_legacy_sensor(hass, ecolink_door_sensor, integration):
"""Test enabled legacy boolean binary sensor."""
node = ecolink_door_sensor
# this node has Notification CC not (fully) implemented
# so legacy binary sensor should be enabled
state = hass.states.get(ENABLED_LEGACY_BINARY_SENSOR)
assert state
assert state.state == STATE_OFF
assert state.attributes.get("device_class") is None
# Test state updates from value updated event
event = Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": 53,
"args": {
"commandClassName": "Binary Sensor",
"commandClass": 48,
"endpoint": 0,
"property": "Any",
"newValue": True,
"prevValue": False,
"propertyName": "Any",
},
},
)
node.receive_event(event)
state = hass.states.get(ENABLED_LEGACY_BINARY_SENSOR)
assert state.state == STATE_ON
async def test_disabled_legacy_sensor(hass, multisensor_6, integration):
"""Test disabled legacy boolean binary sensor."""
# this node has Notification CC implemented so legacy binary sensor should be disabled
registry = await hass.helpers.entity_registry.async_get_registry()
entity_id = DISABLED_LEGACY_BINARY_SENSOR
state = hass.states.get(entity_id)
assert state is None
entry = registry.async_get(entity_id)
assert entry
assert entry.disabled
assert entry.disabled_by == "integration"
# Test enabling legacy entity
updated_entry = registry.async_update_entity(
entry.entity_id, **{"disabled_by": None}
)
assert updated_entry != entry
assert updated_entry.disabled is False
async def test_notification_sensor(hass, multisensor_6, integration):
"""Test binary sensor created from Notification CC."""
state = hass.states.get(NOTIFICATION_MOTION_BINARY_SENSOR)
assert state
assert state.state == STATE_ON
assert state.attributes["device_class"] == DEVICE_CLASS_MOTION

View File

@ -0,0 +1,334 @@
{
"nodeId": 53,
"index": 0,
"status": 1,
"ready": true,
"deviceClass": {
"basic": "Static Controller",
"generic": "Binary Sensor",
"specific": "Routing Binary Sensor",
"mandatorySupportedCCs": [
"Basic",
"Binary Sensor"
],
"mandatoryControlCCs": [
]
},
"isListening": false,
"isFrequentListening": false,
"isRouting": true,
"maxBaudRate": 40000,
"isSecure": false,
"version": 4,
"isBeaming": true,
"manufacturerId": 330,
"productId": 2,
"productType": 1,
"firmwareVersion": "2.0",
"deviceConfig": {
"manufacturerId": 330,
"manufacturer": "Ecolink",
"label": "DWZWAVE2",
"description": "Z-Wave Door/Window Sensor",
"devices": [
{
"productType": "0x0001",
"productId": "0x0002"
}
],
"firmwareVersion": {
"min": "0.0",
"max": "255.255"
},
"associations": {
},
"paramInformation": {
"_map": {
}
}
},
"label": "DWZWAVE2",
"neighbors": [
],
"interviewAttempts": 1,
"endpoints": [
{
"nodeId": 2,
"index": 0
}
],
"values": [
{
"commandClassName": "Basic",
"commandClass": 32,
"endpoint": 0,
"property": "currentValue",
"propertyName": "currentValue",
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"min": 0,
"max": 99,
"label": "Current value"
}
},
{
"commandClassName": "Basic",
"commandClass": 32,
"endpoint": 0,
"property": "targetValue",
"propertyName": "targetValue",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"min": 0,
"max": 99,
"label": "Target value"
}
},
{
"commandClassName": "Binary Sensor",
"commandClass": 48,
"endpoint": 0,
"property": "Any",
"propertyName": "Any",
"metadata": {
"type": "boolean",
"readable": true,
"writeable": false,
"label": "Any",
"ccSpecific": {
"sensorType": 255
}
},
"value": false
},
{
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 1,
"propertyName": "Sending Basic Sets to Association group 2",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"valueSize": 1,
"min": 0,
"max": 255,
"default": 0,
"format": 1,
"allowManualEntry": false,
"states": {
"0": "Off",
"255": "On"
},
"label": "Sending Basic Sets to Association group 2",
"description": "Sending Basic Sets to Association group 2",
"isFromConfig": true
}
},
{
"commandClassName": "Configuration",
"commandClass": 112,
"endpoint": 0,
"property": 2,
"propertyName": "Sending sensor binary report",
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"valueSize": 1,
"min": 0,
"max": 255,
"default": 0,
"format": 1,
"allowManualEntry": false,
"states": {
"0": "Off",
"255": "On"
},
"label": "Sending sensor binary report",
"description": "Sending sensor binary report",
"isFromConfig": true
}
},
{
"commandClassName": "Notification",
"commandClass": 113,
"endpoint": 0,
"property": "Home Security",
"propertyKey": "Cover status",
"propertyName": "Home Security",
"propertyKeyName": "Cover status",
"metadata": {
"type": "any",
"readable": true,
"writeable": true
},
"value": 3
},
{
"commandClassName": "Manufacturer Specific",
"commandClass": 114,
"endpoint": 0,
"property": "manufacturerId",
"propertyName": "manufacturerId",
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"min": 0,
"max": 65535,
"label": "Manufacturer ID"
},
"value": 330
},
{
"commandClassName": "Manufacturer Specific",
"commandClass": 114,
"endpoint": 0,
"property": "productType",
"propertyName": "productType",
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"min": 0,
"max": 65535,
"label": "Product type"
},
"value": 1
},
{
"commandClassName": "Manufacturer Specific",
"commandClass": 114,
"endpoint": 0,
"property": "productId",
"propertyName": "productId",
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"min": 0,
"max": 65535,
"label": "Product ID"
},
"value": 2
},
{
"commandClassName": "Battery",
"commandClass": 128,
"endpoint": 0,
"property": "level",
"propertyName": "level",
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"min": 0,
"max": 100,
"unit": "%",
"label": "Battery level"
},
"value": 61
},
{
"commandClassName": "Battery",
"commandClass": 128,
"endpoint": 0,
"property": "isLow",
"propertyName": "isLow",
"metadata": {
"type": "boolean",
"readable": true,
"writeable": false,
"label": "Low battery level"
},
"value": false
},
{
"commandClassName": "Wake Up",
"commandClass": 132,
"endpoint": 0,
"property": "wakeUpInterval",
"propertyName": "wakeUpInterval",
"metadata": {
"type": "number",
"readable": false,
"writeable": true,
"min": 3600,
"max": 604800,
"label": "Wake Up interval",
"steps": 200,
"default": 14400
},
"value": 14400
},
{
"commandClassName": "Wake Up",
"commandClass": 132,
"endpoint": 0,
"property": "controllerNodeId",
"propertyName": "controllerNodeId",
"metadata": {
"type": "any",
"readable": true,
"writeable": false,
"label": "Node ID of the controller"
},
"value": 1
},
{
"commandClassName": "Version",
"commandClass": 134,
"endpoint": 0,
"property": "libraryType",
"propertyName": "libraryType",
"metadata": {
"type": "any",
"readable": true,
"writeable": false,
"label": "Library type"
},
"value": 6
},
{
"commandClassName": "Version",
"commandClass": 134,
"endpoint": 0,
"property": "protocolVersion",
"propertyName": "protocolVersion",
"metadata": {
"type": "any",
"readable": true,
"writeable": false,
"label": "Z-Wave protocol version"
},
"value": "3.40"
},
{
"commandClassName": "Version",
"commandClass": 134,
"endpoint": 0,
"property": "firmwareVersions",
"propertyName": "firmwareVersions",
"metadata": {
"type": "any",
"readable": true,
"writeable": false,
"label": "Z-Wave chip firmware versions"
},
"value": [
"2.0"
]
}
]
}