From 4c45cb5c5252eebbd00375677ff20fd8e62a2114 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 8 Jun 2022 18:29:46 -0400 Subject: [PATCH] Add UniFi Protect chime button/camera switch (#73195) --- .../components/unifiprotect/button.py | 15 +++++- .../components/unifiprotect/camera.py | 12 ++++- .../components/unifiprotect/switch.py | 8 ++++ tests/components/unifiprotect/test_camera.py | 47 ++++++++++++++++++- tests/components/unifiprotect/test_switch.py | 6 +-- 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 3728b6b4224..9ed5ecc4967 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -43,6 +43,15 @@ ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ), ) +SENSOR_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( + ProtectButtonEntityDescription( + key="clear_tamper", + name="Clear Tamper", + icon="mdi:notification-clear-all", + ufp_press="clear_tamper", + ), +) + CHIME_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ProtectButtonEntityDescription( key="play", @@ -69,7 +78,11 @@ async def async_setup_entry( data: ProtectData = hass.data[DOMAIN][entry.entry_id] entities: list[ProtectDeviceEntity] = async_all_device_entities( - data, ProtectButton, all_descs=ALL_DEVICE_BUTTONS, chime_descs=CHIME_BUTTONS + data, + ProtectButton, + all_descs=ALL_DEVICE_BUTTONS, + chime_descs=CHIME_BUTTONS, + sense_descs=SENSOR_BUTTONS, ) async_add_entities(entities) diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index ca076e490a2..8020d5e8aab 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -140,9 +140,9 @@ class ProtectCamera(ProtectDeviceEntity, Camera): def _async_update_device_from_protect(self) -> None: super()._async_update_device_from_protect() self.channel = self.device.channels[self.channel.id] + motion_enabled = self.device.recording_settings.enable_motion_detection self._attr_motion_detection_enabled = ( - self.device.state == StateType.CONNECTED - and self.device.feature_flags.has_motion_zones + motion_enabled if motion_enabled is not None else True ) self._attr_is_recording = ( self.device.state == StateType.CONNECTED and self.device.is_recording @@ -171,3 +171,11 @@ class ProtectCamera(ProtectDeviceEntity, Camera): async def stream_source(self) -> str | None: """Return the Stream Source.""" return self._stream_source + + async def async_enable_motion_detection(self) -> None: + """Call the job and enable motion detection.""" + await self.device.set_motion_detection(True) + + async def async_disable_motion_detection(self) -> None: + """Call the job and disable motion detection.""" + await self.device.set_motion_detection(False) diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 971c637a8c2..d8542da2f7f 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -133,6 +133,14 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_value="osd_settings.is_debug_enabled", ufp_set_method="set_osd_bitrate", ), + ProtectSwitchEntityDescription( + key="motion", + name="Detections: Motion", + icon="mdi:run-fast", + entity_category=EntityCategory.CONFIG, + ufp_value="recording_settings.enable_motion_detection", + ufp_set_method="set_motion_detection", + ), ProtectSwitchEntityDescription( key="smart_person", name="Detections: Person", diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index d7fc2a62325..538b3bf7652 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -48,6 +48,9 @@ async def camera_fixture( ): """Fixture for a single camera for testing the camera platform.""" + # disable pydantic validation so mocking can happen + ProtectCamera.__config__.validate_assignment = False + camera_obj = mock_camera.copy(deep=True) camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api @@ -68,7 +71,9 @@ async def camera_fixture( assert_entity_counts(hass, Platform.CAMERA, 2, 1) - return (camera_obj, "camera.test_camera_high") + yield (camera_obj, "camera.test_camera_high") + + ProtectCamera.__config__.validate_assignment = True @pytest.fixture(name="camera_package") @@ -572,3 +577,43 @@ async def test_camera_ws_update_offline( state = hass.states.get(camera[1]) assert state and state.state == "idle" + + +async def test_camera_enable_motion( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + camera: tuple[ProtectCamera, str], +): + """Tests generic entity update service.""" + + camera[0].__fields__["set_motion_detection"] = Mock() + camera[0].set_motion_detection = AsyncMock() + + await hass.services.async_call( + "camera", + "enable_motion_detection", + {ATTR_ENTITY_ID: camera[1]}, + blocking=True, + ) + + camera[0].set_motion_detection.assert_called_once_with(True) + + +async def test_camera_disable_motion( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + camera: tuple[ProtectCamera, str], +): + """Tests generic entity update service.""" + + camera[0].__fields__["set_motion_detection"] = Mock() + camera[0].set_motion_detection = AsyncMock() + + await hass.services.async_call( + "camera", + "disable_motion_detection", + {ATTR_ENTITY_ID: camera[1]}, + blocking=True, + ) + + camera[0].set_motion_detection.assert_called_once_with(False) diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 7918ea0b6cf..bc0c8387c29 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -118,7 +118,7 @@ async def camera_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - assert_entity_counts(hass, Platform.SWITCH, 12, 11) + assert_entity_counts(hass, Platform.SWITCH, 13, 12) yield camera_obj @@ -161,7 +161,7 @@ async def camera_none_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - assert_entity_counts(hass, Platform.SWITCH, 5, 4) + assert_entity_counts(hass, Platform.SWITCH, 6, 5) yield camera_obj @@ -205,7 +205,7 @@ async def camera_privacy_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - assert_entity_counts(hass, Platform.SWITCH, 6, 5) + assert_entity_counts(hass, Platform.SWITCH, 7, 6) yield camera_obj