Add motion detection enable/disable to ring camera platform (#108789)

* Add motion detection enable/disable to ring camera platform

* Write ha state directly

Co-authored-by: J. Nick Koston <nick@koston.org>

* Parametrize on off state tests

* Add tests for errors on setting motion detection

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Steven B 2024-03-12 15:05:14 +00:00 committed by GitHub
parent 42574fe498
commit 5e530fc42e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 186 additions and 0 deletions

View file

@ -22,6 +22,7 @@ from .coordinator import RingDataCoordinator
from .entity import RingEntity, exception_wrap
FORCE_REFRESH_INTERVAL = timedelta(minutes=3)
MOTION_DETECTION_CAPABILITY = "motion_detection"
_LOGGER = logging.getLogger(__name__)
@ -67,6 +68,8 @@ class RingCam(RingEntity, Camera):
self._image = None
self._expires_at = dt_util.utcnow() - FORCE_REFRESH_INTERVAL
self._attr_unique_id = device.id
if device.has_capability(MOTION_DETECTION_CAPABILITY):
self._attr_motion_detection_enabled = device.motion_detection
@callback
def _handle_coordinator_update(self):
@ -131,6 +134,13 @@ class RingCam(RingEntity, Camera):
async def async_update(self) -> None:
"""Update camera entity and refresh attributes."""
if (
self._device.has_capability(MOTION_DETECTION_CAPABILITY)
and self._attr_motion_detection_enabled != self._device.motion_detection
):
self._attr_motion_detection_enabled = self._device.motion_detection
self.async_write_ha_state()
if self._last_event is None:
return
@ -152,3 +162,23 @@ class RingCam(RingEntity, Camera):
@exception_wrap
def _get_video(self) -> str:
return self._device.recording_url(self._last_event["id"])
@exception_wrap
def _set_motion_detection_enabled(self, new_state):
if not self._device.has_capability(MOTION_DETECTION_CAPABILITY):
_LOGGER.error(
"Entity %s does not have motion detection capability", self.entity_id
)
return
self._device.motion_detection = new_state
self._attr_motion_detection_enabled = new_state
self.schedule_update_ha_state(False)
def enable_motion_detection(self) -> None:
"""Enable motion detection in the camera."""
self._set_motion_detection_enabled(True)
def disable_motion_detection(self) -> None:
"""Disable motion detection in camera."""
self._set_motion_detection_enabled(False)

View file

@ -121,4 +121,11 @@ def requests_mock_fixture():
status_code=200,
json={"url": "http://127.0.0.1/foo"},
)
# Mocks the response for setting properties in settings (i.e. motion_detection)
mock.patch(
re.compile(
r"https:\/\/api\.ring\.com\/devices\/v1\/devices\/\d+\/settings"
),
text="ok",
)
yield mock

View file

@ -69,6 +69,7 @@
"enable_vod": true,
"live_view_preset_profile": "highest",
"live_view_presets": ["low", "middle", "high", "highest"],
"motion_detection_enabled": true,
"motion_announcement": false,
"motion_snooze_preset_profile": "low",
"motion_snooze_presets": ["null", "low", "medium", "high"]
@ -133,6 +134,7 @@
},
"live_view_preset_profile": "highest",
"live_view_presets": ["low", "middle", "high", "highest"],
"motion_detection_enabled": false,
"motion_announcement": false,
"motion_snooze_preset_profile": "low",
"motion_snooze_presets": ["none", "low", "medium", "high"],
@ -281,6 +283,7 @@
},
"live_view_preset_profile": "highest",
"live_view_presets": ["low", "middle", "high", "highest"],
"motion_detection_enabled": true,
"motion_announcement": false,
"motion_snooze_preset_profile": "low",
"motion_snooze_presets": ["none", "low", "medium", "high"],

View file

@ -69,6 +69,7 @@
"enable_vod": true,
"live_view_preset_profile": "highest",
"live_view_presets": ["low", "middle", "high", "highest"],
"motion_detection_enabled": true,
"motion_announcement": false,
"motion_snooze_preset_profile": "low",
"motion_snooze_presets": ["null", "low", "medium", "high"]
@ -133,6 +134,7 @@
},
"live_view_preset_profile": "highest",
"live_view_presets": ["low", "middle", "high", "highest"],
"motion_detection_enabled": true,
"motion_announcement": false,
"motion_snooze_preset_profile": "low",
"motion_snooze_presets": ["none", "low", "medium", "high"],
@ -281,6 +283,7 @@
},
"live_view_preset_profile": "highest",
"live_view_presets": ["low", "middle", "high", "highest"],
"motion_detection_enabled": false,
"motion_announcement": false,
"motion_snooze_preset_profile": "low",
"motion_snooze_presets": ["none", "low", "medium", "high"],

View file

@ -82,6 +82,7 @@
'highest',
]),
'motion_announcement': False,
'motion_detection_enabled': True,
'motion_snooze_preset_profile': 'low',
'motion_snooze_presets': list([
'null',
@ -158,6 +159,7 @@
'highest',
]),
'motion_announcement': False,
'motion_detection_enabled': False,
'motion_snooze_preset_profile': 'low',
'motion_snooze_presets': list([
'none',
@ -398,6 +400,7 @@
'highest',
]),
'motion_announcement': False,
'motion_detection_enabled': True,
'motion_snooze_preset_profile': 'low',
'motion_snooze_presets': list([
'none',

View file

@ -0,0 +1,140 @@
"""The tests for the Ring switch platform."""
from unittest.mock import PropertyMock, patch
import pytest
import requests_mock
import ring_doorbell
from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from .common import setup_platform
from tests.common import load_fixture
async def test_entity_registry(
hass: HomeAssistant, requests_mock: requests_mock.Mocker
) -> None:
"""Tests that the devices are registered in the entity registry."""
await setup_platform(hass, Platform.CAMERA)
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("camera.front")
assert entry.unique_id == 765432
entry = entity_registry.async_get("camera.internal")
assert entry.unique_id == 345678
@pytest.mark.parametrize(
("entity_name", "expected_state", "friendly_name"),
[
("camera.internal", True, "Internal"),
("camera.front", None, "Front"),
],
ids=["On", "Off"],
)
async def test_camera_motion_detection_state_reports_correctly(
hass: HomeAssistant,
requests_mock: requests_mock.Mocker,
entity_name,
expected_state,
friendly_name,
) -> None:
"""Tests that the initial state of a device that should be off is correct."""
await setup_platform(hass, Platform.CAMERA)
state = hass.states.get(entity_name)
assert state.attributes.get("motion_detection") is expected_state
assert state.attributes.get("friendly_name") == friendly_name
async def test_camera_motion_detection_can_be_turned_on(
hass: HomeAssistant, requests_mock: requests_mock.Mocker
) -> None:
"""Tests the siren turns on correctly."""
await setup_platform(hass, Platform.CAMERA)
state = hass.states.get("camera.front")
assert state.attributes.get("motion_detection") is not True
await hass.services.async_call(
"camera",
"enable_motion_detection",
{"entity_id": "camera.front"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("camera.front")
assert state.attributes.get("motion_detection") is True
async def test_updates_work(
hass: HomeAssistant, requests_mock: requests_mock.Mocker
) -> None:
"""Tests the update service works correctly."""
await setup_platform(hass, Platform.CAMERA)
state = hass.states.get("camera.internal")
assert state.attributes.get("motion_detection") is True
# Changes the return to indicate that the switch is now on.
requests_mock.get(
"https://api.ring.com/clients_api/ring_devices",
text=load_fixture("devices_updated.json", "ring"),
)
await hass.services.async_call("ring", "update", {}, blocking=True)
await hass.async_block_till_done()
state = hass.states.get("camera.internal")
assert state.attributes.get("motion_detection") is not True
@pytest.mark.parametrize(
("exception_type", "reauth_expected"),
[
(ring_doorbell.AuthenticationError, True),
(ring_doorbell.RingTimeout, False),
(ring_doorbell.RingError, False),
],
ids=["Authentication", "Timeout", "Other"],
)
async def test_motion_detection_errors_when_turned_on(
hass: HomeAssistant,
requests_mock: requests_mock.Mocker,
exception_type,
reauth_expected,
) -> None:
"""Tests the motion detection errors are handled correctly."""
await setup_platform(hass, Platform.CAMERA)
config_entry = hass.config_entries.async_entries("ring")[0]
assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
with patch.object(
ring_doorbell.RingDoorBell, "motion_detection", new_callable=PropertyMock
) as mock_motion_detection:
mock_motion_detection.side_effect = exception_type
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
"camera",
"enable_motion_detection",
{"entity_id": "camera.front"},
blocking=True,
)
await hass.async_block_till_done()
assert mock_motion_detection.call_count == 1
assert (
any(
flow
for flow in config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})
if flow["handler"] == "ring"
)
== reauth_expected
)