core/tests/components/tplink/test_camera.py

455 lines
14 KiB
Python

"""The tests for the tplink camera platform."""
import asyncio
from unittest.mock import AsyncMock, patch
from aiohttp.test_utils import make_mocked_request
from freezegun.api import FrozenDateTimeFactory
from kasa import Module
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components import stream
from homeassistant.components.camera import (
DOMAIN as CAMERA_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
CameraEntityFeature,
StreamType,
async_get_image,
async_get_mjpeg_stream,
get_camera_from_entity_id,
)
from homeassistant.components.tplink.camera import TPLinkCameraEntity
from homeassistant.components.websocket_api import TYPE_RESULT
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from . import _mocked_device, setup_platform_for_device, snapshot_platform
from .const import DEVICE_ID, IP_ADDRESS3, MAC_ADDRESS3, SMALLEST_VALID_JPEG_BYTES
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.typing import WebSocketGenerator
async def test_states(
hass: HomeAssistant,
mock_camera_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test camera states."""
mock_camera_config_entry.add_to_hass(hass)
mock_device = _mocked_device(
modules=[Module.Camera],
alias="my_camera",
ip_address=IP_ADDRESS3,
mac=MAC_ADDRESS3,
)
# Patch getrandbits so the access_token doesn't change on camera attributes
with patch("random.SystemRandom.getrandbits", return_value=123123123123):
await setup_platform_for_device(
hass, mock_camera_config_entry, Platform.CAMERA, mock_device
)
await snapshot_platform(
hass,
entity_registry,
device_registry,
snapshot,
mock_camera_config_entry.entry_id,
)
async def test_camera_unique_id(
hass: HomeAssistant,
mock_camera_config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test camera unique id."""
mock_device = _mocked_device(
modules=[Module.Camera],
alias="my_camera",
ip_address=IP_ADDRESS3,
mac=MAC_ADDRESS3,
device_id=DEVICE_ID,
)
await setup_platform_for_device(
hass, mock_camera_config_entry, Platform.CAMERA, mock_device
)
device_entries = dr.async_entries_for_config_entry(
device_registry, mock_camera_config_entry.entry_id
)
assert device_entries
entity_id = "camera.my_camera_live_view"
assert entity_registry.async_get(entity_id).unique_id == f"{DEVICE_ID}-live_view"
async def test_handle_mjpeg_stream(
hass: HomeAssistant,
mock_camera_config_entry: MockConfigEntry,
) -> None:
"""Test handle_async_mjpeg_stream."""
mock_device = _mocked_device(
modules=[Module.Camera],
alias="my_camera",
ip_address=IP_ADDRESS3,
mac=MAC_ADDRESS3,
)
await setup_platform_for_device(
hass, mock_camera_config_entry, Platform.CAMERA, mock_device
)
state = hass.states.get("camera.my_camera_live_view")
assert state is not None
mock_request = make_mocked_request("GET", "/", headers={"token": "x"})
stream = await async_get_mjpeg_stream(
hass, mock_request, "camera.my_camera_live_view"
)
assert stream is not None
async def test_handle_mjpeg_stream_not_supported(
hass: HomeAssistant,
mock_camera_config_entry: MockConfigEntry,
) -> None:
"""Test no stream if stream_rtsp_url is None after creation."""
mock_device = _mocked_device(
modules=[Module.Camera],
alias="my_camera",
ip_address=IP_ADDRESS3,
mac=MAC_ADDRESS3,
)
mock_camera = mock_device.modules[Module.Camera]
mock_camera.stream_rtsp_url.side_effect = ("foo", None)
await setup_platform_for_device(
hass, mock_camera_config_entry, Platform.CAMERA, mock_device
)
mock_request = make_mocked_request("GET", "/", headers={"token": "x"})
mjpeg_stream = await async_get_mjpeg_stream(
hass, mock_request, "camera.my_camera_live_view"
)
assert mjpeg_stream is None
async def test_camera_image(
hass: HomeAssistant,
mock_camera_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test async_get_image."""
mock_device = _mocked_device(
modules=[Module.Camera],
alias="my_camera",
ip_address=IP_ADDRESS3,
mac=MAC_ADDRESS3,
)
await setup_platform_for_device(
hass, mock_camera_config_entry, Platform.CAMERA, mock_device
)
state = hass.states.get("camera.my_camera_live_view")
assert state is not None
with patch(
"homeassistant.components.ffmpeg.async_get_image",
return_value=SMALLEST_VALID_JPEG_BYTES,
) as mock_get_image:
image = await async_get_image(hass, "camera.my_camera_live_view")
assert image
assert image.content == SMALLEST_VALID_JPEG_BYTES
mock_get_image.assert_called_once()
mock_get_image.reset_mock()
image = await async_get_image(hass, "camera.my_camera_live_view")
mock_get_image.assert_not_called()
freezer.tick(TPLinkCameraEntity.IMAGE_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
mock_get_image.reset_mock()
image = await async_get_image(hass, "camera.my_camera_live_view")
mock_get_image.assert_called_once()
freezer.tick(TPLinkCameraEntity.IMAGE_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# Test image returns None
with patch(
"homeassistant.components.ffmpeg.async_get_image",
return_value=None,
) as mock_get_image:
msg = f"None camera image returned for {IP_ADDRESS3}"
assert msg not in caplog.text
mock_get_image.reset_mock()
image = await async_get_image(hass, "camera.my_camera_live_view")
mock_get_image.assert_called_once()
assert msg in caplog.text
async def test_no_camera_image_when_streaming(
hass: HomeAssistant,
mock_camera_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test no camera image when streaming."""
mock_device = _mocked_device(
modules=[Module.Camera],
alias="my_camera",
ip_address=IP_ADDRESS3,
mac=MAC_ADDRESS3,
)
await setup_platform_for_device(
hass, mock_camera_config_entry, Platform.CAMERA, mock_device
)
state = hass.states.get("camera.my_camera_live_view")
assert state is not None
with patch(
"homeassistant.components.ffmpeg.async_get_image",
return_value=SMALLEST_VALID_JPEG_BYTES,
) as mock_get_image:
await async_get_image(hass, "camera.my_camera_live_view")
mock_get_image.assert_called_once()
freezer.tick(TPLinkCameraEntity.IMAGE_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
start_event = asyncio.Event()
finish_event = asyncio.Event()
async def _waiter(*_, **__):
start_event.set()
await finish_event.wait()
async def _get_stream():
mock_request = make_mocked_request("GET", "/", headers={"token": "x"})
await async_get_mjpeg_stream(
hass, mock_request, "camera.my_camera_live_view"
)
mock_get_image.reset_mock()
with patch(
"homeassistant.components.tplink.camera.async_aiohttp_proxy_stream",
new=_waiter,
):
task = asyncio.create_task(_get_stream())
await start_event.wait()
await async_get_image(hass, "camera.my_camera_live_view")
finish_event.set()
await task
mock_get_image.assert_not_called()
async def test_no_concurrent_camera_image(
hass: HomeAssistant,
mock_camera_config_entry: MockConfigEntry,
) -> None:
"""Test async_get_image doesn't make concurrent requests."""
mock_device = _mocked_device(
modules=[Module.Camera],
alias="my_camera",
ip_address=IP_ADDRESS3,
mac=MAC_ADDRESS3,
)
await setup_platform_for_device(
hass, mock_camera_config_entry, Platform.CAMERA, mock_device
)
state = hass.states.get("camera.my_camera_live_view")
assert state is not None
finish_event = asyncio.Event()
call_count = 0
async def _waiter(*_, **__):
nonlocal call_count
call_count += 1
await finish_event.wait()
return SMALLEST_VALID_JPEG_BYTES
with patch(
"homeassistant.components.ffmpeg.async_get_image",
new=_waiter,
):
tasks = asyncio.gather(
async_get_image(hass, "camera.my_camera_live_view"),
async_get_image(hass, "camera.my_camera_live_view"),
)
# Sleep to give both tasks chance to get to th asyncio.Lock()
await asyncio.sleep(0)
finish_event.set()
results = await tasks
assert len(results) == 2
assert all(img and img.content == SMALLEST_VALID_JPEG_BYTES for img in results)
assert call_count == 1
async def test_camera_image_auth_error(
hass: HomeAssistant,
mock_camera_config_entry: MockConfigEntry,
mock_connect: AsyncMock,
mock_discovery: AsyncMock,
) -> None:
"""Test async_get_image auth error."""
mock_device = _mocked_device(
modules=[Module.Camera],
alias="my_camera",
ip_address=IP_ADDRESS3,
mac=MAC_ADDRESS3,
)
await setup_platform_for_device(
hass, mock_camera_config_entry, Platform.CAMERA, mock_device
)
state = hass.states.get("camera.my_camera_live_view")
assert state is not None
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 0
with (
patch(
"homeassistant.components.ffmpeg.async_get_image",
return_value=b"",
),
patch(
"homeassistant.components.stream.async_check_stream_client_error",
side_effect=stream.StreamOpenClientError(
"Request was unauthorized",
error_code=stream.StreamClientError.Unauthorized,
),
),
pytest.raises(HomeAssistantError),
):
await async_get_image(hass, "camera.my_camera_live_view")
await hass.async_block_till_done()
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
[result] = flows
assert result["step_id"] == "camera_auth_confirm"
async def test_camera_stream_source(
hass: HomeAssistant,
mock_camera_config_entry: MockConfigEntry,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test camera stream source.
This test would fail if the integration didn't properly
put stream in the dependencies.
"""
mock_device = _mocked_device(
modules=[Module.Camera],
alias="my_camera",
ip_address=IP_ADDRESS3,
mac=MAC_ADDRESS3,
)
await setup_platform_for_device(
hass, mock_camera_config_entry, Platform.CAMERA, mock_device
)
state = hass.states.get("camera.my_camera_live_view")
assert state is not None
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "camera/stream", "entity_id": "camera.my_camera_live_view"}
)
msg = await client.receive_json()
# Assert WebSocket response
assert msg["type"] == TYPE_RESULT
assert msg["success"]
assert "url" in msg["result"]
async def test_camera_stream_attributes(
hass: HomeAssistant,
mock_camera_config_entry: MockConfigEntry,
) -> None:
"""Test stream attributes."""
mock_device = _mocked_device(
modules=[Module.Camera],
alias="my_camera",
ip_address=IP_ADDRESS3,
mac=MAC_ADDRESS3,
)
await setup_platform_for_device(
hass, mock_camera_config_entry, Platform.CAMERA, mock_device
)
state = hass.states.get("camera.my_camera_live_view")
assert state is not None
supported_features = state.attributes.get("supported_features")
assert supported_features is CameraEntityFeature.STREAM | CameraEntityFeature.ON_OFF
camera = get_camera_from_entity_id(hass, "camera.my_camera_live_view")
assert camera.camera_capabilities.frontend_stream_types == {StreamType.HLS}
async def test_camera_turn_on_off(
hass: HomeAssistant,
mock_camera_config_entry: MockConfigEntry,
) -> None:
"""Test camera turn on and off."""
mock_device = _mocked_device(
modules=[Module.Camera],
alias="my_camera",
ip_address=IP_ADDRESS3,
mac=MAC_ADDRESS3,
)
mock_camera = mock_device.modules[Module.Camera]
await setup_platform_for_device(
hass, mock_camera_config_entry, Platform.CAMERA, mock_device
)
state = hass.states.get("camera.my_camera_live_view")
assert state is not None
await hass.services.async_call(
CAMERA_DOMAIN,
SERVICE_TURN_ON,
{"entity_id": "camera.my_camera_live_view"},
blocking=True,
)
mock_camera.set_state.assert_called_with(True)
await hass.services.async_call(
CAMERA_DOMAIN,
SERVICE_TURN_OFF,
{"entity_id": "camera.my_camera_live_view"},
blocking=True,
)
mock_camera.set_state.assert_called_with(False)