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:
parent
42574fe498
commit
5e530fc42e
6 changed files with 186 additions and 0 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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',
|
||||
|
|
140
tests/components/ring/test_camera.py
Normal file
140
tests/components/ring/test_camera.py
Normal 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
|
||||
)
|
Loading…
Add table
Reference in a new issue