Parametrize deCONZ binary sensors (#65012)

* Improve test coverage prior to improving deCONZ binary sensor platform

* Define all relevant binary sensors as DeconzBinarySensorDescription

* Fix review comment

* Allow providing extra update keys if sensor provides extra attributes

* Minor touch up of naming

* Remove duplicate assert
This commit is contained in:
Robert Svensson 2022-02-04 12:58:07 +01:00 committed by GitHub
parent b7007b364a
commit 96c4e33b24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 623 additions and 300 deletions

View File

@ -7,7 +7,6 @@ from dataclasses import dataclass
from pydeconz.sensor import ( from pydeconz.sensor import (
Alarm, Alarm,
CarbonMonoxide, CarbonMonoxide,
DeconzBinarySensor as PydeconzBinarySensor,
DeconzSensor as PydeconzSensor, DeconzSensor as PydeconzSensor,
Fire, Fire,
GenericFlag, GenericFlag,
@ -34,21 +33,21 @@ from .const import ATTR_DARK, ATTR_ON
from .deconz_device import DeconzDevice from .deconz_device import DeconzDevice
from .gateway import DeconzGateway, get_gateway_from_config_entry from .gateway import DeconzGateway, get_gateway_from_config_entry
DECONZ_BINARY_SENSORS = (
Alarm,
CarbonMonoxide,
Fire,
GenericFlag,
OpenClose,
Presence,
Vibration,
Water,
)
ATTR_ORIENTATION = "orientation" ATTR_ORIENTATION = "orientation"
ATTR_TILTANGLE = "tiltangle" ATTR_TILTANGLE = "tiltangle"
ATTR_VIBRATIONSTRENGTH = "vibrationstrength" ATTR_VIBRATIONSTRENGTH = "vibrationstrength"
PROVIDES_EXTRA_ATTRIBUTES = (
"alarm",
"carbon_monoxide",
"fire",
"flag",
"open",
"presence",
"vibration",
"water",
)
@dataclass @dataclass
class DeconzBinarySensorDescriptionMixin: class DeconzBinarySensorDescriptionMixin:
@ -56,7 +55,6 @@ class DeconzBinarySensorDescriptionMixin:
suffix: str suffix: str
update_key: str update_key: str
required_attr: str
value_fn: Callable[[PydeconzSensor], bool | None] value_fn: Callable[[PydeconzSensor], bool | None]
@ -69,41 +67,90 @@ class DeconzBinarySensorDescription(
ENTITY_DESCRIPTIONS = { ENTITY_DESCRIPTIONS = {
Alarm: BinarySensorEntityDescription( Alarm: [
key="alarm", DeconzBinarySensorDescription(
device_class=BinarySensorDeviceClass.SAFETY, key="alarm",
), value_fn=lambda device: device.alarm,
CarbonMonoxide: BinarySensorEntityDescription( suffix="",
key="carbonmonoxide", update_key="alarm",
device_class=BinarySensorDeviceClass.CO, device_class=BinarySensorDeviceClass.SAFETY,
), )
Fire: BinarySensorEntityDescription( ],
key="fire", CarbonMonoxide: [
device_class=BinarySensorDeviceClass.SMOKE, DeconzBinarySensorDescription(
), key="carbon_monoxide",
OpenClose: BinarySensorEntityDescription( value_fn=lambda device: device.carbon_monoxide,
key="openclose", suffix="",
device_class=BinarySensorDeviceClass.OPENING, update_key="carbonmonoxide",
), device_class=BinarySensorDeviceClass.CO,
Presence: BinarySensorEntityDescription( )
key="presence", ],
device_class=BinarySensorDeviceClass.MOTION, Fire: [
), DeconzBinarySensorDescription(
Vibration: BinarySensorEntityDescription( key="fire",
key="vibration", value_fn=lambda device: device.fire,
device_class=BinarySensorDeviceClass.VIBRATION, suffix="",
), update_key="fire",
Water: BinarySensorEntityDescription( device_class=BinarySensorDeviceClass.SMOKE,
key="water", ),
device_class=BinarySensorDeviceClass.MOISTURE, DeconzBinarySensorDescription(
), key="in_test_mode",
value_fn=lambda device: device.in_test_mode,
suffix="Test Mode",
update_key="test",
device_class=BinarySensorDeviceClass.SMOKE,
entity_category=EntityCategory.DIAGNOSTIC,
),
],
GenericFlag: [
DeconzBinarySensorDescription(
key="flag",
value_fn=lambda device: device.flag,
suffix="",
update_key="flag",
)
],
OpenClose: [
DeconzBinarySensorDescription(
key="open",
value_fn=lambda device: device.open,
suffix="",
update_key="open",
device_class=BinarySensorDeviceClass.OPENING,
)
],
Presence: [
DeconzBinarySensorDescription(
key="presence",
value_fn=lambda device: device.presence,
suffix="",
update_key="presence",
device_class=BinarySensorDeviceClass.MOTION,
)
],
Vibration: [
DeconzBinarySensorDescription(
key="vibration",
value_fn=lambda device: device.vibration,
suffix="",
update_key="vibration",
device_class=BinarySensorDeviceClass.VIBRATION,
)
],
Water: [
DeconzBinarySensorDescription(
key="water",
value_fn=lambda device: device.water,
suffix="",
update_key="water",
device_class=BinarySensorDeviceClass.MOISTURE,
)
],
} }
BINARY_SENSOR_DESCRIPTIONS = [ BINARY_SENSOR_DESCRIPTIONS = [
DeconzBinarySensorDescription( DeconzBinarySensorDescription(
key="tamper", key="tampered",
required_attr="tampered",
value_fn=lambda device: device.tampered, value_fn=lambda device: device.tampered,
suffix="Tampered", suffix="Tampered",
update_key="tampered", update_key="tampered",
@ -112,22 +159,12 @@ BINARY_SENSOR_DESCRIPTIONS = [
), ),
DeconzBinarySensorDescription( DeconzBinarySensorDescription(
key="low_battery", key="low_battery",
required_attr="low_battery",
value_fn=lambda device: device.low_battery, value_fn=lambda device: device.low_battery,
suffix="Low Battery", suffix="Low Battery",
update_key="lowbattery", update_key="lowbattery",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
DeconzBinarySensorDescription(
key="in_test_mode",
required_attr="in_test_mode",
value_fn=lambda device: device.in_test_mode,
suffix="Test Mode",
update_key="test",
device_class=BinarySensorDeviceClass.SMOKE,
entity_category=EntityCategory.DIAGNOSTIC,
),
] ]
@ -146,32 +183,26 @@ async def async_setup_entry(
| ValuesView[PydeconzSensor] = gateway.api.sensors.values(), | ValuesView[PydeconzSensor] = gateway.api.sensors.values(),
) -> None: ) -> None:
"""Add binary sensor from deCONZ.""" """Add binary sensor from deCONZ."""
entities: list[DeconzBinarySensor | DeconzPropertyBinarySensor] = [] entities: list[DeconzBinarySensor] = []
for sensor in sensors: for sensor in sensors:
if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"):
continue continue
if ( known_entities = set(gateway.entities[DOMAIN])
isinstance(sensor, DECONZ_BINARY_SENSORS) for description in (
and sensor.unique_id not in gateway.entities[DOMAIN] ENTITY_DESCRIPTIONS.get(type(sensor), []) + BINARY_SENSOR_DESCRIPTIONS
): ):
entities.append(DeconzBinarySensor(sensor, gateway))
known_sensor_entities = set(gateway.entities[DOMAIN])
for sensor_description in BINARY_SENSOR_DESCRIPTIONS:
if ( if (
not hasattr(sensor, sensor_description.required_attr) not hasattr(sensor, description.key)
or sensor_description.value_fn(sensor) is None or description.value_fn(sensor) is None
): ):
continue continue
new_sensor = DeconzPropertyBinarySensor( new_sensor = DeconzBinarySensor(sensor, gateway, description)
sensor, gateway, sensor_description if new_sensor.unique_id not in known_entities:
)
if new_sensor.unique_id not in known_sensor_entities:
entities.append(new_sensor) entities.append(new_sensor)
if entities: if entities:
@ -194,30 +225,50 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity):
"""Representation of a deCONZ binary sensor.""" """Representation of a deCONZ binary sensor."""
TYPE = DOMAIN TYPE = DOMAIN
_device: PydeconzBinarySensor _device: PydeconzSensor
entity_description: DeconzBinarySensorDescription
def __init__(self, device: PydeconzBinarySensor, gateway: DeconzGateway) -> None: def __init__(
self,
device: PydeconzSensor,
gateway: DeconzGateway,
description: DeconzBinarySensorDescription,
) -> None:
"""Initialize deCONZ binary sensor.""" """Initialize deCONZ binary sensor."""
self.entity_description: DeconzBinarySensorDescription = description
super().__init__(device, gateway) super().__init__(device, gateway)
if entity_description := ENTITY_DESCRIPTIONS.get(type(device)): if description.suffix:
self.entity_description = entity_description self._attr_name = f"{self._device.name} {description.suffix}"
self._update_keys = {description.update_key, "reachable"}
if self.entity_description.key in PROVIDES_EXTRA_ATTRIBUTES:
self._update_keys.update({"on", "state"})
@property
def unique_id(self) -> str:
"""Return a unique identifier for this device."""
if self.entity_description.suffix:
return f"{self.serial}-{self.entity_description.suffix.lower()}"
return super().unique_id
@callback @callback
def async_update_callback(self) -> None: def async_update_callback(self) -> None:
"""Update the sensor's state.""" """Update the sensor's state."""
keys = {"on", "reachable", "state"} if self._device.changed_keys.intersection(self._update_keys):
if self._device.changed_keys.intersection(keys):
super().async_update_callback() super().async_update_callback()
@property @property
def is_on(self) -> bool: def is_on(self) -> bool | None:
"""Return true if sensor is on.""" """Return the state of the sensor."""
return self._device.state # type: ignore[no-any-return] return self.entity_description.value_fn(self._device)
@property @property
def extra_state_attributes(self) -> dict[str, bool | float | int | list | None]: def extra_state_attributes(self) -> dict[str, bool | float | int | list | None]:
"""Return the state attributes of the sensor.""" """Return the state attributes of the sensor."""
if self.entity_description.key not in PROVIDES_EXTRA_ATTRIBUTES:
return
attr: dict[str, bool | float | int | list | None] = {} attr: dict[str, bool | float | int | list | None] = {}
if self._device.on is not None: if self._device.on is not None:
@ -237,40 +288,3 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity):
attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibration_strength attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibration_strength
return attr return attr
class DeconzPropertyBinarySensor(DeconzDevice, BinarySensorEntity):
"""Representation of a deCONZ Property sensor."""
TYPE = DOMAIN
_device: PydeconzSensor
entity_description: DeconzBinarySensorDescription
def __init__(
self,
device: PydeconzSensor,
gateway: DeconzGateway,
description: DeconzBinarySensorDescription,
) -> None:
"""Initialize deCONZ binary sensor."""
self.entity_description = description
super().__init__(device, gateway)
self._attr_name = f"{self._device.name} {description.suffix}"
self._update_keys = {description.update_key, "reachable"}
@property
def unique_id(self) -> str:
"""Return a unique identifier for this device."""
return f"{self.serial}-{self.entity_description.suffix.lower()}"
@callback
def async_update_callback(self) -> None:
"""Update the sensor's state."""
if self._device.changed_keys.intersection(self._update_keys):
super().async_update_callback()
@property
def is_on(self) -> bool | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self._device)

View File

@ -2,6 +2,8 @@
from unittest.mock import patch from unittest.mock import patch
import pytest
from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.deconz.const import ( from homeassistant.components.deconz.const import (
CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_CLIP_SENSOR,
@ -10,14 +12,13 @@ from homeassistant.components.deconz.const import (
DOMAIN as DECONZ_DOMAIN, DOMAIN as DECONZ_DOMAIN,
) )
from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_DEVICE_CLASS,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
) )
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.helpers.entity_registry import async_entries_for_config_entry
@ -34,204 +35,512 @@ async def test_no_binary_sensors(hass, aioclient_mock):
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 0
async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): TEST_DATA = [
( # Alarm binary sensor
{
"config": {
"battery": 100,
"on": True,
"reachable": True,
"temperature": 2600,
},
"ep": 1,
"etag": "18c0f3c2100904e31a7f938db2ba9ba9",
"manufacturername": "dresden elektronik",
"modelid": "lumi.sensor_motion.aq2",
"name": "Alarm 10",
"state": {
"alarm": False,
"lastupdated": "none",
"lowbattery": None,
"tampered": None,
},
"swversion": "20170627",
"type": "ZHAAlarm",
"uniqueid": "00:15:8d:00:02:b5:d1:80-01-0500",
},
{
"entity_count": 3,
"device_count": 3,
"entity_id": "binary_sensor.alarm_10",
"unique_id": "00:15:8d:00:02:b5:d1:80-01-0500",
"state": STATE_OFF,
"entity_category": None,
"device_class": BinarySensorDeviceClass.SAFETY,
"attributes": {
"on": True,
"temperature": 26.0,
"device_class": "safety",
"friendly_name": "Alarm 10",
},
"websocket_event": {"alarm": True},
"next_state": STATE_ON,
},
),
( # Carbon monoxide binary sensor
{
"config": {
"battery": 100,
"on": True,
"pending": [],
"reachable": True,
},
"ep": 1,
"etag": "b7599df551944df97b2aa87d160b9c45",
"manufacturername": "Heiman",
"modelid": "CO_V16",
"name": "Cave CO",
"state": {
"carbonmonoxide": False,
"lastupdated": "none",
"lowbattery": False,
"tampered": False,
},
"swversion": "20150330",
"type": "ZHACarbonMonoxide",
"uniqueid": "00:15:8d:00:02:a5:21:24-01-0101",
},
{
"entity_count": 4,
"device_count": 3,
"entity_id": "binary_sensor.cave_co",
"unique_id": "00:15:8d:00:02:a5:21:24-01-0101",
"state": STATE_OFF,
"entity_category": None,
"device_class": BinarySensorDeviceClass.CO,
"attributes": {
"on": True,
"device_class": "carbon_monoxide",
"friendly_name": "Cave CO",
},
"websocket_event": {"carbonmonoxide": True},
"next_state": STATE_ON,
},
),
( # Fire binary sensor
{
"config": {
"on": True,
"reachable": True,
},
"ep": 1,
"etag": "2b585d2c016bfd665ba27a8fdad28670",
"manufacturername": "LUMI",
"modelid": "lumi.sensor_smoke",
"name": "sensor_kitchen_smoke",
"state": {
"fire": False,
"lastupdated": "2018-02-20T11:25:02",
},
"type": "ZHAFire",
"uniqueid": "00:15:8d:00:01:d9:3e:7c-01-0500",
},
{
"entity_count": 2,
"device_count": 3,
"entity_id": "binary_sensor.sensor_kitchen_smoke",
"unique_id": "00:15:8d:00:01:d9:3e:7c-01-0500",
"state": STATE_OFF,
"entity_category": None,
"device_class": BinarySensorDeviceClass.SMOKE,
"attributes": {
"on": True,
"device_class": "smoke",
"friendly_name": "sensor_kitchen_smoke",
},
"websocket_event": {"fire": True},
"next_state": STATE_ON,
},
),
( # Fire test mode binary sensor
{
"config": {
"on": True,
"reachable": True,
},
"ep": 1,
"etag": "2b585d2c016bfd665ba27a8fdad28670",
"manufacturername": "LUMI",
"modelid": "lumi.sensor_smoke",
"name": "sensor_kitchen_smoke",
"state": {
"fire": False,
"test": False,
"lastupdated": "2018-02-20T11:25:02",
},
"type": "ZHAFire",
"uniqueid": "00:15:8d:00:01:d9:3e:7c-01-0500",
},
{
"entity_count": 2,
"device_count": 3,
"entity_id": "binary_sensor.sensor_kitchen_smoke_test_mode",
"unique_id": "00:15:8d:00:01:d9:3e:7c-test mode",
"state": STATE_OFF,
"entity_category": EntityCategory.DIAGNOSTIC,
"device_class": BinarySensorDeviceClass.SMOKE,
"attributes": {
"device_class": "smoke",
"friendly_name": "sensor_kitchen_smoke Test Mode",
},
"websocket_event": {"test": True},
"next_state": STATE_ON,
},
),
( # Generic flag binary sensor
{
"config": {
"on": True,
"reachable": True,
},
"modelid": "Switch",
"name": "Kitchen Switch",
"state": {
"flag": True,
"lastupdated": "2018-07-01T10:40:35",
},
"swversion": "1.0.0",
"type": "CLIPGenericFlag",
"uniqueid": "kitchen-switch",
},
{
"entity_count": 1,
"device_count": 2,
"entity_id": "binary_sensor.kitchen_switch",
"unique_id": "kitchen-switch",
"state": STATE_ON,
"entity_category": None,
"device_class": None,
"attributes": {
"on": True,
"friendly_name": "Kitchen Switch",
},
"websocket_event": {"flag": False},
"next_state": STATE_OFF,
},
),
( # Open/Close binary sensor
{
"config": {
"battery": 95,
"on": True,
"reachable": True,
"temperature": 3300,
},
"ep": 1,
"etag": "66cc641d0368110da6882b50090174ac",
"manufacturername": "LUMI",
"modelid": "lumi.sensor_magnet.aq2",
"name": "Back Door",
"state": {
"lastupdated": "2019-05-05T14:54:32",
"open": False,
},
"swversion": "20161128",
"type": "ZHAOpenClose",
"uniqueid": "00:15:8d:00:02:2b:96:b4-01-0006",
},
{
"entity_count": 3,
"device_count": 3,
"entity_id": "binary_sensor.back_door",
"unique_id": "00:15:8d:00:02:2b:96:b4-01-0006",
"state": STATE_OFF,
"entity_category": None,
"device_class": BinarySensorDeviceClass.OPENING,
"attributes": {
"on": True,
"temperature": 33.0,
"device_class": "opening",
"friendly_name": "Back Door",
},
"websocket_event": {"open": True},
"next_state": STATE_ON,
},
),
( # Presence binary sensor
{
"config": {
"alert": "none",
"battery": 100,
"delay": 0,
"ledindication": False,
"on": True,
"pending": [],
"reachable": True,
"sensitivity": 1,
"sensitivitymax": 2,
"usertest": False,
},
"ep": 2,
"etag": "5cfb81765e86aa53ace427cfd52c6d52",
"manufacturername": "Philips",
"modelid": "SML001",
"name": "Motion sensor 4",
"state": {
"dark": False,
"lastupdated": "2019-05-05T14:37:06",
"presence": False,
},
"swversion": "6.1.0.18912",
"type": "ZHAPresence",
"uniqueid": "00:17:88:01:03:28:8c:9b-02-0406",
},
{
"entity_count": 3,
"device_count": 3,
"entity_id": "binary_sensor.motion_sensor_4",
"unique_id": "00:17:88:01:03:28:8c:9b-02-0406",
"state": STATE_OFF,
"entity_category": None,
"device_class": BinarySensorDeviceClass.MOTION,
"attributes": {
"on": True,
"dark": False,
"device_class": "motion",
"friendly_name": "Motion sensor 4",
},
"websocket_event": {"presence": True},
"next_state": STATE_ON,
},
),
( # Water leak binary sensor
{
"config": {
"battery": 100,
"on": True,
"reachable": True,
"temperature": 2500,
},
"ep": 1,
"etag": "fae893708dfe9b358df59107d944fa1c",
"manufacturername": "LUMI",
"modelid": "lumi.sensor_wleak.aq1",
"name": "water2",
"state": {
"lastupdated": "2019-01-29T07:13:20",
"lowbattery": False,
"tampered": False,
"water": False,
},
"swversion": "20170721",
"type": "ZHAWater",
"uniqueid": "00:15:8d:00:02:2f:07:db-01-0500",
},
{
"entity_count": 5,
"device_count": 3,
"entity_id": "binary_sensor.water2",
"unique_id": "00:15:8d:00:02:2f:07:db-01-0500",
"state": STATE_OFF,
"entity_category": None,
"device_class": BinarySensorDeviceClass.MOISTURE,
"attributes": {
"on": True,
"temperature": 25.0,
"device_class": "moisture",
"friendly_name": "water2",
},
"websocket_event": {"water": True},
"next_state": STATE_ON,
},
),
( # Vibration binary sensor
{
"config": {
"battery": 91,
"on": True,
"pending": [],
"reachable": True,
"sensitivity": 21,
"sensitivitymax": 21,
"temperature": 3200,
},
"ep": 1,
"etag": "b7599df551944df97b2aa87d160b9c45",
"manufacturername": "LUMI",
"modelid": "lumi.vibration.aq1",
"name": "Vibration 1",
"state": {
"lastupdated": "2019-03-09T15:53:07",
"orientation": [10, 1059, 0],
"tiltangle": 83,
"vibration": True,
"vibrationstrength": 114,
},
"swversion": "20180130",
"type": "ZHAVibration",
"uniqueid": "00:15:8d:00:02:a5:21:24-01-0101",
},
{
"entity_count": 3,
"device_count": 3,
"entity_id": "binary_sensor.vibration_1",
"unique_id": "00:15:8d:00:02:a5:21:24-01-0101",
"state": STATE_ON,
"entity_category": None,
"device_class": BinarySensorDeviceClass.VIBRATION,
"attributes": {
"on": True,
"temperature": 32.0,
"orientation": [10, 1059, 0],
"tiltangle": 83,
"vibrationstrength": 114,
"device_class": "vibration",
"friendly_name": "Vibration 1",
},
"websocket_event": {"vibration": False},
"next_state": STATE_OFF,
},
),
( # Tampering binary sensor
{
"name": "Presence sensor",
"type": "ZHAPresence",
"state": {
"dark": False,
"lowbattery": False,
"presence": False,
"tampered": False,
},
"config": {
"on": True,
"reachable": True,
"temperature": 10,
},
"uniqueid": "00:00:00:00:00:00:00:00-00",
},
{
"entity_count": 4,
"device_count": 3,
"entity_id": "binary_sensor.presence_sensor_tampered",
"unique_id": "00:00:00:00:00:00:00:00-tampered",
"state": STATE_OFF,
"entity_category": EntityCategory.DIAGNOSTIC,
"device_class": BinarySensorDeviceClass.TAMPER,
"attributes": {
"device_class": "tamper",
"friendly_name": "Presence sensor Tampered",
},
"websocket_event": {"tampered": True},
"next_state": STATE_ON,
},
),
( # Low battery binary sensor
{
"name": "Presence sensor",
"type": "ZHAPresence",
"state": {
"dark": False,
"lowbattery": False,
"presence": False,
"tampered": False,
},
"config": {
"on": True,
"reachable": True,
"temperature": 10,
},
"uniqueid": "00:00:00:00:00:00:00:00-00",
},
{
"entity_count": 4,
"device_count": 3,
"entity_id": "binary_sensor.presence_sensor_low_battery",
"unique_id": "00:00:00:00:00:00:00:00-low battery",
"state": STATE_OFF,
"entity_category": EntityCategory.DIAGNOSTIC,
"device_class": BinarySensorDeviceClass.BATTERY,
"attributes": {
"device_class": "battery",
"friendly_name": "Presence sensor Low Battery",
},
"websocket_event": {"lowbattery": True},
"next_state": STATE_ON,
},
),
]
@pytest.mark.parametrize("sensor_data, expected", TEST_DATA)
async def test_binary_sensors(
hass, aioclient_mock, mock_deconz_websocket, sensor_data, expected
):
"""Test successful creation of binary sensor entities.""" """Test successful creation of binary sensor entities."""
ent_reg = er.async_get(hass)
dev_reg = dr.async_get(hass)
with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"1": sensor_data}}):
config_entry = await setup_deconz_integration(
hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True}
)
assert len(hass.states.async_all()) == expected["entity_count"]
# Verify state data
sensor = hass.states.get(expected["entity_id"])
assert sensor.state == expected["state"]
assert sensor.attributes.get(ATTR_DEVICE_CLASS) == expected["device_class"]
assert sensor.attributes == expected["attributes"]
# Verify entity registry data
ent_reg_entry = ent_reg.async_get(expected["entity_id"])
assert ent_reg_entry.entity_category is expected["entity_category"]
assert ent_reg_entry.unique_id == expected["unique_id"]
# Verify device registry data
assert (
len(dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id))
== expected["device_count"]
)
# Change state
event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "1",
"state": expected["websocket_event"],
}
await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get(expected["entity_id"]).state == expected["next_state"]
# Unload entry
await hass.config_entries.async_unload(config_entry.entry_id)
assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE
# Remove entry
await hass.config_entries.async_remove(config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
async def test_not_allow_clip_sensor(hass, aioclient_mock):
"""Test that CLIP sensors are not allowed."""
data = { data = {
"sensors": { "sensors": {
"1": { "1": {
"name": "Presence sensor",
"type": "ZHAPresence",
"state": {"dark": False, "presence": False},
"config": {"on": True, "reachable": True, "temperature": 10},
"uniqueid": "00:00:00:00:00:00:00:00-00",
},
"2": {
"name": "Temperature sensor",
"type": "ZHATemperature",
"state": {"temperature": False},
"config": {},
"uniqueid": "00:00:00:00:00:00:00:01-00",
},
"3": {
"name": "CLIP presence sensor", "name": "CLIP presence sensor",
"type": "CLIPPresence", "type": "CLIPPresence",
"state": {"presence": False}, "state": {"presence": False},
"config": {}, "config": {},
"uniqueid": "00:00:00:00:00:00:00:02-00", "uniqueid": "00:00:00:00:00:00:00:02-00",
}, },
"4": {
"name": "Vibration sensor",
"type": "ZHAVibration",
"state": {
"orientation": [1, 2, 3],
"tiltangle": 36,
"vibration": True,
"vibrationstrength": 10,
},
"config": {"on": True, "reachable": True, "temperature": 10},
"uniqueid": "00:00:00:00:00:00:00:03-00",
},
} }
} }
with patch.dict(DECONZ_WEB_REQUEST, data): with patch.dict(DECONZ_WEB_REQUEST, data):
config_entry = await setup_deconz_integration(hass, aioclient_mock) await setup_deconz_integration(
hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: False}
)
assert len(hass.states.async_all()) == 5
presence_sensor = hass.states.get("binary_sensor.presence_sensor")
assert presence_sensor.state == STATE_OFF
assert (
presence_sensor.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.MOTION
)
presence_temp = hass.states.get("sensor.presence_sensor_temperature")
assert presence_temp.state == "0.1"
assert presence_temp.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE
assert hass.states.get("binary_sensor.temperature_sensor") is None
assert hass.states.get("binary_sensor.clip_presence_sensor") is None
vibration_sensor = hass.states.get("binary_sensor.vibration_sensor")
assert vibration_sensor.state == STATE_ON
assert (
vibration_sensor.attributes[ATTR_DEVICE_CLASS]
== BinarySensorDeviceClass.VIBRATION
)
vibration_temp = hass.states.get("sensor.vibration_sensor_temperature")
assert vibration_temp.state == "0.1"
assert vibration_temp.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE
event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "1",
"state": {"presence": True},
}
await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("binary_sensor.presence_sensor").state == STATE_ON
await hass.config_entries.async_unload(config_entry.entry_id)
assert hass.states.get("binary_sensor.presence_sensor").state == STATE_UNAVAILABLE
await hass.config_entries.async_remove(config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
async def test_tampering_sensor(hass, aioclient_mock, mock_deconz_websocket):
"""Verify tampering sensor works."""
data = {
"sensors": {
"1": {
"name": "Presence sensor",
"type": "ZHAPresence",
"state": {
"dark": False,
"lowbattery": False,
"presence": False,
"tampered": False,
},
"config": {"on": True, "reachable": True, "temperature": 10},
"uniqueid": "00:00:00:00:00:00:00:00-00",
},
}
}
with patch.dict(DECONZ_WEB_REQUEST, data):
config_entry = await setup_deconz_integration(hass, aioclient_mock)
ent_reg = er.async_get(hass)
assert len(hass.states.async_all()) == 4
hass.states.get("binary_sensor.presence_sensor_low_battery").state == STATE_OFF
assert (
ent_reg.async_get("binary_sensor.presence_sensor_low_battery").entity_category
is EntityCategory.DIAGNOSTIC
)
presence_tamper = hass.states.get("binary_sensor.presence_sensor_tampered")
assert presence_tamper.state == STATE_OFF
assert (
presence_tamper.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.TAMPER
)
assert (
ent_reg.async_get("binary_sensor.presence_sensor_tampered").entity_category
is EntityCategory.DIAGNOSTIC
)
event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "1",
"state": {"tampered": True},
}
await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("binary_sensor.presence_sensor_tampered").state == STATE_ON
await hass.config_entries.async_unload(config_entry.entry_id)
assert (
hass.states.get("binary_sensor.presence_sensor_tampered").state
== STATE_UNAVAILABLE
)
await hass.config_entries.async_remove(config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
async def test_fire_sensor(hass, aioclient_mock, mock_deconz_websocket):
"""Verify smoke alarm sensor works."""
data = {
"sensors": {
"1": {
"name": "Fire alarm",
"type": "ZHAFire",
"state": {"fire": False, "test": False},
"config": {"on": True, "reachable": True},
"uniqueid": "00:00:00:00:00:00:00:00-00",
},
}
}
with patch.dict(DECONZ_WEB_REQUEST, data):
config_entry = await setup_deconz_integration(hass, aioclient_mock)
ent_reg = er.async_get(hass)
assert len(hass.states.async_all()) == 2
assert hass.states.get("binary_sensor.fire_alarm").state == STATE_OFF
assert ent_reg.async_get("binary_sensor.fire_alarm").entity_category is None
assert hass.states.get("binary_sensor.fire_alarm_test_mode").state == STATE_OFF
assert (
ent_reg.async_get("binary_sensor.fire_alarm_test_mode").entity_category
is EntityCategory.DIAGNOSTIC
)
event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "1",
"state": {"fire": True, "test": True},
}
await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("binary_sensor.fire_alarm").state == STATE_ON
assert hass.states.get("binary_sensor.fire_alarm_test_mode").state == STATE_ON
await hass.config_entries.async_unload(config_entry.entry_id)
assert hass.states.get("binary_sensor.fire_alarm").state == STATE_UNAVAILABLE
assert (
hass.states.get("binary_sensor.fire_alarm_test_mode").state == STATE_UNAVAILABLE
)
await hass.config_entries.async_remove(config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 0