diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 7bf05ee6edd..de23d06e7db 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -6,6 +6,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, + DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SMOKE, DEVICE_CLASS_VIBRATION, DOMAIN, @@ -55,6 +56,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ): entities.append(DeconzBinarySensor(sensor, gateway)) + if sensor.tampered is not None: + known_tampering_sensors = set(gateway.entities[DOMAIN]) + new_tampering_sensor = DeconzTampering(sensor, gateway) + if new_tampering_sensor.unique_id not in known_tampering_sensors: + entities.append(new_tampering_sensor) + if entities: async_add_entities(entities) @@ -113,3 +120,36 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibrationstrength return attr + + +class DeconzTampering(DeconzDevice, BinarySensorEntity): + """Representation of a deCONZ tampering sensor.""" + + TYPE = DOMAIN + + @property + def unique_id(self) -> str: + """Return a unique identifier for this device.""" + return f"{self.serial}-tampered" + + @callback + def async_update_callback(self, force_update: bool = False) -> None: + """Update the sensor's state.""" + keys = {"tampered", "reachable"} + if force_update or self._device.changed_keys.intersection(keys): + super().async_update_callback(force_update=force_update) + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self._device.tampered + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return f"{self._device.name} Tampered" + + @property + def device_class(self) -> str: + """Return the class of the sensor.""" + return DEVICE_CLASS_PROBLEM diff --git a/tests/components/deconz/test_alarm_control_panel.py b/tests/components/deconz/test_alarm_control_panel.py index 6d6b09466a5..b8de1f10d85 100644 --- a/tests/components/deconz/test_alarm_control_panel.py +++ b/tests/components/deconz/test_alarm_control_panel.py @@ -82,7 +82,7 @@ async def test_alarm_control_panel(hass, aioclient_mock, mock_deconz_websocket): with patch.dict(DECONZ_WEB_REQUEST, data): config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 2 assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_DISARMED # Event signals alarm control panel armed away @@ -261,7 +261,7 @@ async def test_alarm_control_panel(hass, aioclient_mock, mock_deconz_websocket): await hass.config_entries.async_unload(config_entry.entry_id) states = hass.states.async_all() - assert len(states) == 1 + assert len(states) == 2 for state in states: assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 6ba79dfe4ab..2a1b3c154f0 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -4,6 +4,7 @@ from unittest.mock import patch from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, + DEVICE_CLASS_PROBLEM, DEVICE_CLASS_VIBRATION, ) from homeassistant.components.deconz.const import ( @@ -116,6 +117,52 @@ async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): 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, "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) + + assert len(hass.states.async_all()) == 3 + presence_tamper = hass.states.get("binary_sensor.presence_sensor_tampered") + assert presence_tamper.state == STATE_OFF + assert presence_tamper.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_PROBLEM + + 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_allow_clip_sensor(hass, aioclient_mock): """Test that CLIP sensors can be allowed.""" data = { diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 4024b516498..798a96a43d7 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -232,7 +232,7 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): device_registry = await hass.helpers.device_registry.async_get_registry() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 2 # 1 alarm control device + 2 additional devices for deconz service and host assert ( len(async_entries_for_config_entry(device_registry, config_entry.entry_id)) == 3 @@ -294,7 +294,7 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): await hass.config_entries.async_unload(config_entry.entry_id) states = hass.states.async_all() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 2 for state in states: assert state.state == STATE_UNAVAILABLE