diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index cda743f0893..fd674dc1cba 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -7,7 +7,6 @@ from dataclasses import dataclass from pydeconz.sensor import ( Alarm, CarbonMonoxide, - DeconzBinarySensor as PydeconzBinarySensor, DeconzSensor as PydeconzSensor, Fire, GenericFlag, @@ -34,21 +33,21 @@ from .const import ATTR_DARK, ATTR_ON from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry -DECONZ_BINARY_SENSORS = ( - Alarm, - CarbonMonoxide, - Fire, - GenericFlag, - OpenClose, - Presence, - Vibration, - Water, -) - ATTR_ORIENTATION = "orientation" ATTR_TILTANGLE = "tiltangle" ATTR_VIBRATIONSTRENGTH = "vibrationstrength" +PROVIDES_EXTRA_ATTRIBUTES = ( + "alarm", + "carbon_monoxide", + "fire", + "flag", + "open", + "presence", + "vibration", + "water", +) + @dataclass class DeconzBinarySensorDescriptionMixin: @@ -56,7 +55,6 @@ class DeconzBinarySensorDescriptionMixin: suffix: str update_key: str - required_attr: str value_fn: Callable[[PydeconzSensor], bool | None] @@ -69,41 +67,90 @@ class DeconzBinarySensorDescription( ENTITY_DESCRIPTIONS = { - Alarm: BinarySensorEntityDescription( - key="alarm", - device_class=BinarySensorDeviceClass.SAFETY, - ), - CarbonMonoxide: BinarySensorEntityDescription( - key="carbonmonoxide", - device_class=BinarySensorDeviceClass.CO, - ), - Fire: BinarySensorEntityDescription( - key="fire", - device_class=BinarySensorDeviceClass.SMOKE, - ), - OpenClose: BinarySensorEntityDescription( - key="openclose", - device_class=BinarySensorDeviceClass.OPENING, - ), - Presence: BinarySensorEntityDescription( - key="presence", - device_class=BinarySensorDeviceClass.MOTION, - ), - Vibration: BinarySensorEntityDescription( - key="vibration", - device_class=BinarySensorDeviceClass.VIBRATION, - ), - Water: BinarySensorEntityDescription( - key="water", - device_class=BinarySensorDeviceClass.MOISTURE, - ), + Alarm: [ + DeconzBinarySensorDescription( + key="alarm", + value_fn=lambda device: device.alarm, + suffix="", + update_key="alarm", + device_class=BinarySensorDeviceClass.SAFETY, + ) + ], + CarbonMonoxide: [ + DeconzBinarySensorDescription( + key="carbon_monoxide", + value_fn=lambda device: device.carbon_monoxide, + suffix="", + update_key="carbonmonoxide", + device_class=BinarySensorDeviceClass.CO, + ) + ], + Fire: [ + DeconzBinarySensorDescription( + key="fire", + value_fn=lambda device: device.fire, + suffix="", + update_key="fire", + device_class=BinarySensorDeviceClass.SMOKE, + ), + 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 = [ DeconzBinarySensorDescription( - key="tamper", - required_attr="tampered", + key="tampered", value_fn=lambda device: device.tampered, suffix="Tampered", update_key="tampered", @@ -112,22 +159,12 @@ BINARY_SENSOR_DESCRIPTIONS = [ ), DeconzBinarySensorDescription( key="low_battery", - required_attr="low_battery", value_fn=lambda device: device.low_battery, suffix="Low Battery", update_key="lowbattery", device_class=BinarySensorDeviceClass.BATTERY, 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(), ) -> None: """Add binary sensor from deCONZ.""" - entities: list[DeconzBinarySensor | DeconzPropertyBinarySensor] = [] + entities: list[DeconzBinarySensor] = [] for sensor in sensors: if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): continue - if ( - isinstance(sensor, DECONZ_BINARY_SENSORS) - and sensor.unique_id not in gateway.entities[DOMAIN] + known_entities = set(gateway.entities[DOMAIN]) + for description in ( + 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 ( - not hasattr(sensor, sensor_description.required_attr) - or sensor_description.value_fn(sensor) is None + not hasattr(sensor, description.key) + or description.value_fn(sensor) is None ): continue - new_sensor = DeconzPropertyBinarySensor( - sensor, gateway, sensor_description - ) - if new_sensor.unique_id not in known_sensor_entities: + new_sensor = DeconzBinarySensor(sensor, gateway, description) + if new_sensor.unique_id not in known_entities: entities.append(new_sensor) if entities: @@ -194,30 +225,50 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): """Representation of a deCONZ binary sensor.""" 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.""" + self.entity_description: DeconzBinarySensorDescription = description super().__init__(device, gateway) - if entity_description := ENTITY_DESCRIPTIONS.get(type(device)): - self.entity_description = entity_description + if description.suffix: + 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 def async_update_callback(self) -> None: """Update the sensor's state.""" - keys = {"on", "reachable", "state"} - if self._device.changed_keys.intersection(keys): + if self._device.changed_keys.intersection(self._update_keys): super().async_update_callback() @property - def is_on(self) -> bool: - """Return true if sensor is on.""" - return self._device.state # type: ignore[no-any-return] + def is_on(self) -> bool | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self._device) @property def extra_state_attributes(self) -> dict[str, bool | float | int | list | None]: """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] = {} if self._device.on is not None: @@ -237,40 +288,3 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibration_strength 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) diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 11f9483e277..0bd308caeed 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -2,6 +2,8 @@ from unittest.mock import patch +import pytest + from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, @@ -10,14 +12,13 @@ from homeassistant.components.deconz.const import ( DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH -from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON, 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_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 -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.""" + 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 = { "sensors": { "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", "type": "CLIPPresence", "state": {"presence": False}, "config": {}, "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): - 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