diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 37ba103f4ba..1dcaa7ce2a3 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -41,4 +41,4 @@ DEVICES_FOR_SUBSCRIBE = DEVICES_WITH_ENTITIES | {ModelType.EVENT} MIN_REQUIRED_PROTECT_V = Version("1.20.0") OUTDATED_LOG_MESSAGE = "You are running v%s of UniFi Protect. Minimum required version is v%s. Please upgrade UniFi Protect and then retry" -PLATFORMS = [Platform.CAMERA] +PLATFORMS = [Platform.CAMERA, Platform.MEDIA_PLAYER] diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 41d698b61e2..a173680e3fb 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", "requirements": [ - "pyunifiprotect==1.4.4" + "pyunifiprotect==1.4.7" ], "codeowners": [ "@briis", diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py new file mode 100644 index 00000000000..63490a80d3c --- /dev/null +++ b/homeassistant/components/unifiprotect/media_player.py @@ -0,0 +1,139 @@ +"""Support for Ubiquiti's UniFi Protect NVR.""" +from __future__ import annotations + +from collections.abc import Callable, Sequence +import logging +from typing import Any + +from pyunifiprotect.data import Camera +from pyunifiprotect.exceptions import StreamError + +from homeassistant.components.media_player import ( + DEVICE_CLASS_SPEAKER, + MediaPlayerEntity, + MediaPlayerEntityDescription, +) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, + SUPPORT_PLAY_MEDIA, + SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_IDLE, STATE_PLAYING +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN +from .data import ProtectData +from .entity import ProtectDeviceEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[Sequence[Entity]], None], +) -> None: + """Discover cameras with speakers on a UniFi Protect NVR.""" + data: ProtectData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + [ + ProtectMediaPlayer( + data, + camera, + ) + for camera in data.api.bootstrap.cameras.values() + if camera.feature_flags.has_speaker + ] + ) + + +class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): + """A Ubiquiti UniFi Protect Speaker.""" + + def __init__( + self, + data: ProtectData, + camera: Camera, + ) -> None: + """Initialize an UniFi speaker.""" + + self.device: Camera = camera + self.entity_description = MediaPlayerEntityDescription( + key="speaker", device_class=DEVICE_CLASS_SPEAKER + ) + super().__init__(data) + + self._attr_name = f"{self.device.name} Speaker" + self._attr_supported_features = ( + SUPPORT_PLAY_MEDIA + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP + | SUPPORT_STOP + | SUPPORT_SELECT_SOURCE + ) + self._attr_media_content_type = MEDIA_TYPE_MUSIC + + @callback + def _async_update_device_from_protect(self) -> None: + super()._async_update_device_from_protect() + self._attr_volume_level = float(self.device.speaker_settings.volume / 100) + + if ( + self.device.talkback_stream is not None + and self.device.talkback_stream.is_running + ): + self._attr_state = STATE_PLAYING + else: + self._attr_state = STATE_IDLE + + @callback + async def async_set_volume_level(self, volume: float) -> None: + """Set volume level, range 0..1.""" + + volume_int = int(volume * 100) + await self.device.set_speaker_volume(volume_int) + + async def async_media_stop(self) -> None: + """Send stop command.""" + + if ( + self.device.talkback_stream is not None + and self.device.talkback_stream.is_running + ): + _LOGGER.debug("Stopping playback for %s Speaker", self.device.name) + await self.device.stop_audio() + self._async_updated_event() + + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: + """Play a piece of media.""" + + if media_type != MEDIA_TYPE_MUSIC: + _LOGGER.warning( + "%s: Cannot play media type of %s, only `%s` supported", + self.device.name, + media_type, + MEDIA_TYPE_MUSIC, + ) + return + + _LOGGER.debug("Playing Media %s for %s Speaker", media_id, self.device.name) + await self.async_media_stop() + try: + await self.device.play_audio(media_id, blocking=False) + except StreamError as err: + _LOGGER.error("Error while playing media: %s", err) + else: + # update state after starting player + self._async_updated_event() + # wait until player finishes to update state again + await self.device.wait_until_audio_completes() + + self._async_updated_event() diff --git a/requirements_all.txt b/requirements_all.txt index 9cf8961131a..90d4c66bdbe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2000,7 +2000,7 @@ pytrafikverket==0.1.6.2 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==1.4.4 +pyunifiprotect==1.4.7 # homeassistant.components.uptimerobot pyuptimerobot==21.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5af8d9d2d12..768a8b3898c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1213,7 +1213,7 @@ pytrafikverket==0.1.6.2 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==1.4.4 +pyunifiprotect==1.4.7 # homeassistant.components.uptimerobot pyuptimerobot==21.11.0 diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index ac4e1491b38..cbc3dff5431 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -95,7 +95,9 @@ def mock_client(): @pytest.fixture -def mock_entry(hass: HomeAssistant, mock_client): +def mock_entry( + hass: HomeAssistant, mock_client # pylint: disable=redefined-outer-name +): """Mock ProtectApiClient for testing.""" with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: @@ -123,44 +125,12 @@ def mock_camera(): """Mock UniFi Protect Camera device.""" path = Path(__file__).parent / "sample_data" / "sample_camera.json" - with open(path, encoding="utf-8") as f: - data = json.load(f) + with open(path, encoding="utf-8") as json_file: + data = json.load(json_file) yield Camera.from_unifi_dict(**data) -@pytest.fixture -async def simple_camera( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera -): - """Fixture for a single camera, no extra setup.""" - - camera = mock_camera.copy(deep=True) - camera._api = mock_entry.api - camera.channels[0]._api = mock_entry.api - camera.channels[1]._api = mock_entry.api - camera.channels[2]._api = mock_entry.api - camera.name = "Test Camera" - camera.channels[0].is_rtsp_enabled = True - camera.channels[0].name = "High" - camera.channels[1].is_rtsp_enabled = False - camera.channels[2].is_rtsp_enabled = False - - mock_entry.api.bootstrap.cameras = { - camera.id: camera, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - entity_registry = er.async_get(hass) - - assert len(hass.states.async_all()) == 1 - assert len(entity_registry.entities) == 2 - - yield (camera, "camera.test_camera_high") - - async def time_changed(hass: HomeAssistant, seconds: int) -> None: """Trigger time changed.""" next_update = dt_util.utcnow() + timedelta(seconds) diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 1ab9e56ff11..df1f5aceb95 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -1,9 +1,11 @@ """Test the UniFi Protect camera platform.""" +# pylint: disable=protected-access from __future__ import annotations from copy import copy -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, patch +import pytest from pyunifiprotect.data import Camera as ProtectCamera from pyunifiprotect.data.devices import CameraChannel from pyunifiprotect.exceptions import NvrError @@ -27,6 +29,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -35,17 +38,50 @@ from homeassistant.setup import async_setup_component from .conftest import MockEntityFixture, enable_entity, time_changed +@pytest.fixture(name="camera") +async def camera_fixture( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera +): + """Fixture for a single camera, no extra setup.""" + + camera_obj = mock_camera.copy(deep=True) + camera_obj._api = mock_entry.api + camera_obj.channels[0]._api = mock_entry.api + camera_obj.channels[1]._api = mock_entry.api + camera_obj.channels[2]._api = mock_entry.api + camera_obj.name = "Test Camera" + camera_obj.channels[0].is_rtsp_enabled = True + camera_obj.channels[0].name = "High" + camera_obj.channels[1].is_rtsp_enabled = False + camera_obj.channels[2].is_rtsp_enabled = False + + mock_entry.api.bootstrap.cameras = { + camera_obj.id: camera_obj, + } + + with patch("homeassistant.components.unifiprotect.PLATFORMS", [Platform.CAMERA]): + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + assert len(hass.states.async_all()) == 1 + assert len(entity_registry.entities) == 2 + + yield (camera_obj, "camera.test_camera_high") + + def validate_default_camera_entity( hass: HomeAssistant, - camera: ProtectCamera, + camera_obj: ProtectCamera, channel_id: int, ) -> str: """Validate a camera entity.""" - channel = camera.channels[channel_id] + channel = camera_obj.channels[channel_id] - entity_name = f"{camera.name} {channel.name}" - unique_id = f"{camera.id}_{channel.id}" + entity_name = f"{camera_obj.name} {channel.name}" + unique_id = f"{camera_obj.id}_{channel.id}" entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" entity_registry = er.async_get(hass) @@ -59,15 +95,15 @@ def validate_default_camera_entity( def validate_rtsps_camera_entity( hass: HomeAssistant, - camera: ProtectCamera, + camera_obj: ProtectCamera, channel_id: int, ) -> str: """Validate a disabled RTSPS camera entity.""" - channel = camera.channels[channel_id] + channel = camera_obj.channels[channel_id] - entity_name = f"{camera.name} {channel.name}" - unique_id = f"{camera.id}_{channel.id}" + entity_name = f"{camera_obj.name} {channel.name}" + unique_id = f"{camera_obj.id}_{channel.id}" entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" entity_registry = er.async_get(hass) @@ -81,15 +117,15 @@ def validate_rtsps_camera_entity( def validate_rtsp_camera_entity( hass: HomeAssistant, - camera: ProtectCamera, + camera_obj: ProtectCamera, channel_id: int, ) -> str: """Validate a disabled RTSP camera entity.""" - channel = camera.channels[channel_id] + channel = camera_obj.channels[channel_id] - entity_name = f"{camera.name} {channel.name} Insecure" - unique_id = f"{camera.id}_{channel.id}_insecure" + entity_name = f"{camera_obj.name} {channel.name} Insecure" + unique_id = f"{camera_obj.id}_{channel.id}_insecure" entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" entity_registry = er.async_get(hass) @@ -121,13 +157,13 @@ def validate_common_camera_state( async def validate_rtsps_camera_state( hass: HomeAssistant, - camera: ProtectCamera, + camera_obj: ProtectCamera, channel_id: int, entity_id: str, features: int = SUPPORT_STREAM, ): """Validate a camera's state.""" - channel = camera.channels[channel_id] + channel = camera_obj.channels[channel_id] assert await async_get_stream_source(hass, entity_id) == channel.rtsps_url validate_common_camera_state(hass, channel, entity_id, features) @@ -135,13 +171,13 @@ async def validate_rtsps_camera_state( async def validate_rtsp_camera_state( hass: HomeAssistant, - camera: ProtectCamera, + camera_obj: ProtectCamera, channel_id: int, entity_id: str, features: int = SUPPORT_STREAM, ): """Validate a camera's state.""" - channel = camera.channels[channel_id] + channel = camera_obj.channels[channel_id] assert await async_get_stream_source(hass, entity_id) == channel.rtsp_url validate_common_camera_state(hass, channel, entity_id, features) @@ -149,13 +185,13 @@ async def validate_rtsp_camera_state( async def validate_no_stream_camera_state( hass: HomeAssistant, - camera: ProtectCamera, + camera_obj: ProtectCamera, channel_id: int, entity_id: str, features: int = SUPPORT_STREAM, ): """Validate a camera's state.""" - channel = camera.channels[channel_id] + channel = camera_obj.channels[channel_id] assert await async_get_stream_source(hass, entity_id) is None validate_common_camera_state(hass, channel, entity_id, features) @@ -228,8 +264,9 @@ async def test_basic_setup( camera_no_channels.id: camera_no_channels, } - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() + with patch("homeassistant.components.unifiprotect.PLATFORMS", [Platform.CAMERA]): + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() entity_registry = er.async_get(hass) @@ -305,52 +342,52 @@ async def test_missing_channels( async def test_camera_image( hass: HomeAssistant, mock_entry: MockEntityFixture, - simple_camera: tuple[Camera, str], + camera: tuple[Camera, str], ): """Test retrieving camera image.""" mock_entry.api.get_camera_snapshot = AsyncMock() - await async_get_image(hass, simple_camera[1]) + await async_get_image(hass, camera[1]) mock_entry.api.get_camera_snapshot.assert_called_once() async def test_camera_generic_update( hass: HomeAssistant, mock_entry: MockEntityFixture, - simple_camera: tuple[ProtectCamera, str], + camera: tuple[ProtectCamera, str], ): """Tests generic entity update service.""" assert await async_setup_component(hass, "homeassistant", {}) - state = hass.states.get(simple_camera[1]) + state = hass.states.get(camera[1]) assert state and state.state == "idle" mock_entry.api.update = AsyncMock(return_value=None) await hass.services.async_call( "homeassistant", "update_entity", - {ATTR_ENTITY_ID: simple_camera[1]}, + {ATTR_ENTITY_ID: camera[1]}, blocking=True, ) - state = hass.states.get(simple_camera[1]) + state = hass.states.get(camera[1]) assert state and state.state == "idle" async def test_camera_interval_update( hass: HomeAssistant, mock_entry: MockEntityFixture, - simple_camera: tuple[ProtectCamera, str], + camera: tuple[ProtectCamera, str], ): """Interval updates updates camera entity.""" - state = hass.states.get(simple_camera[1]) + state = hass.states.get(camera[1]) assert state and state.state == "idle" new_bootstrap = copy(mock_entry.api.bootstrap) - new_camera = simple_camera[0].copy() + new_camera = camera[0].copy() new_camera.is_recording = True new_bootstrap.cameras = {new_camera.id: new_camera} @@ -358,47 +395,47 @@ async def test_camera_interval_update( mock_entry.api.bootstrap = new_bootstrap await time_changed(hass, DEFAULT_SCAN_INTERVAL) - state = hass.states.get(simple_camera[1]) + state = hass.states.get(camera[1]) assert state and state.state == "recording" async def test_camera_bad_interval_update( hass: HomeAssistant, mock_entry: MockEntityFixture, - simple_camera: tuple[Camera, str], + camera: tuple[Camera, str], ): """Interval updates marks camera unavailable.""" - state = hass.states.get(simple_camera[1]) + state = hass.states.get(camera[1]) assert state and state.state == "idle" # update fails mock_entry.api.update = AsyncMock(side_effect=NvrError) await time_changed(hass, DEFAULT_SCAN_INTERVAL) - state = hass.states.get(simple_camera[1]) + state = hass.states.get(camera[1]) assert state and state.state == "unavailable" # next update succeeds mock_entry.api.update = AsyncMock(return_value=mock_entry.api.bootstrap) await time_changed(hass, DEFAULT_SCAN_INTERVAL) - state = hass.states.get(simple_camera[1]) + state = hass.states.get(camera[1]) assert state and state.state == "idle" async def test_camera_ws_update( hass: HomeAssistant, mock_entry: MockEntityFixture, - simple_camera: tuple[ProtectCamera, str], + camera: tuple[ProtectCamera, str], ): """WS update updates camera entity.""" - state = hass.states.get(simple_camera[1]) + state = hass.states.get(camera[1]) assert state and state.state == "idle" new_bootstrap = copy(mock_entry.api.bootstrap) - new_camera = simple_camera[0].copy() + new_camera = camera[0].copy() new_camera.is_recording = True mock_msg = Mock() @@ -409,23 +446,23 @@ async def test_camera_ws_update( mock_entry.api.ws_subscription(mock_msg) await hass.async_block_till_done() - state = hass.states.get(simple_camera[1]) + state = hass.states.get(camera[1]) assert state and state.state == "recording" async def test_camera_ws_update_offline( hass: HomeAssistant, mock_entry: MockEntityFixture, - simple_camera: tuple[ProtectCamera, str], + camera: tuple[ProtectCamera, str], ): """WS updates marks camera unavailable.""" - state = hass.states.get(simple_camera[1]) + state = hass.states.get(camera[1]) assert state and state.state == "idle" # camera goes offline new_bootstrap = copy(mock_entry.api.bootstrap) - new_camera = simple_camera[0].copy() + new_camera = camera[0].copy() new_camera.is_connected = False mock_msg = Mock() @@ -436,7 +473,7 @@ async def test_camera_ws_update_offline( mock_entry.api.ws_subscription(mock_msg) await hass.async_block_till_done() - state = hass.states.get(simple_camera[1]) + state = hass.states.get(camera[1]) assert state and state.state == "unavailable" # camera comes back online @@ -450,5 +487,5 @@ async def test_camera_ws_update_offline( mock_entry.api.ws_subscription(mock_msg) await hass.async_block_till_done() - state = hass.states.get(simple_camera[1]) + state = hass.states.get(camera[1]) assert state and state.state == "idle" diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py new file mode 100644 index 00000000000..fc6429eec96 --- /dev/null +++ b/tests/components/unifiprotect/test_media_player.py @@ -0,0 +1,241 @@ +"""Test the UniFi Protect button platform.""" +# pylint: disable=protected-access +from __future__ import annotations + +from copy import copy +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from pyunifiprotect.data import Camera +from pyunifiprotect.exceptions import StreamError + +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_VOLUME_LEVEL, +) +from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + STATE_IDLE, + STATE_PLAYING, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import MockEntityFixture + + +@pytest.fixture(name="camera") +async def camera_fixture( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera +): + """Fixture for a single camera with only the media_player platform active, camera has speaker.""" + + # disable pydantic validation so mocking can happen + Camera.__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 + camera_obj.channels[1]._api = mock_entry.api + camera_obj.channels[2]._api = mock_entry.api + camera_obj.name = "Test Camera" + camera_obj.feature_flags.has_speaker = True + + mock_entry.api.bootstrap.cameras = { + camera_obj.id: camera_obj, + } + + with patch( + "homeassistant.components.unifiprotect.PLATFORMS", [Platform.MEDIA_PLAYER] + ): + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + assert len(hass.states.async_all()) == 1 + assert len(entity_registry.entities) == 1 + + yield (camera_obj, "media_player.test_camera_speaker") + + Camera.__config__.validate_assignment = True + + +async def test_media_player_setup( + hass: HomeAssistant, + camera: tuple[Camera, str], +): + """Test media_player entity setup.""" + + unique_id = f"{camera[0].id}_speaker" + entity_id = camera[1] + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == unique_id + + expected_volume = float(camera[0].speaker_settings.volume / 100) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_IDLE + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 7684 + assert state.attributes[ATTR_MEDIA_CONTENT_TYPE] == "music" + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == expected_volume + + +async def test_media_player_update( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + camera: tuple[Camera, str], +): + """Test media_player entity update.""" + + new_bootstrap = copy(mock_entry.api.bootstrap) + new_camera = camera[0].copy() + new_camera.talkback_stream = Mock() + new_camera.talkback_stream.is_running = True + + mock_msg = Mock() + mock_msg.new_obj = new_camera + + new_bootstrap.cameras = {new_camera.id: new_camera} + mock_entry.api.bootstrap = new_bootstrap + mock_entry.api.ws_subscription(mock_msg) + await hass.async_block_till_done() + + state = hass.states.get(camera[1]) + assert state + assert state.state == STATE_PLAYING + + +async def test_media_player_set_volume( + hass: HomeAssistant, + camera: tuple[Camera, str], +): + """Test media_player entity test set_volume_level.""" + + camera[0].__fields__["set_speaker_volume"] = Mock() + camera[0].set_speaker_volume = AsyncMock() + + await hass.services.async_call( + "media_player", + "volume_set", + {ATTR_ENTITY_ID: camera[1], "volume_level": 0.5}, + blocking=True, + ) + + camera[0].set_speaker_volume.assert_called_once_with(50) + + +async def test_media_player_stop( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + camera: tuple[Camera, str], +): + """Test media_player entity test media_stop.""" + + new_bootstrap = copy(mock_entry.api.bootstrap) + new_camera = camera[0].copy() + new_camera.talkback_stream = AsyncMock() + new_camera.talkback_stream.is_running = True + + mock_msg = Mock() + mock_msg.new_obj = new_camera + + new_bootstrap.cameras = {new_camera.id: new_camera} + mock_entry.api.bootstrap = new_bootstrap + mock_entry.api.ws_subscription(mock_msg) + await hass.async_block_till_done() + + await hass.services.async_call( + "media_player", + "media_stop", + {ATTR_ENTITY_ID: camera[1]}, + blocking=True, + ) + + new_camera.talkback_stream.stop.assert_called_once() + + +async def test_media_player_play( + hass: HomeAssistant, + camera: tuple[Camera, str], +): + """Test media_player entity test play_media.""" + + camera[0].__fields__["stop_audio"] = Mock() + camera[0].__fields__["play_audio"] = Mock() + camera[0].__fields__["wait_until_audio_completes"] = Mock() + camera[0].stop_audio = AsyncMock() + camera[0].play_audio = AsyncMock() + camera[0].wait_until_audio_completes = AsyncMock() + + await hass.services.async_call( + "media_player", + "play_media", + { + ATTR_ENTITY_ID: camera[1], + "media_content_id": "/test.mp3", + "media_content_type": "music", + }, + blocking=True, + ) + + camera[0].play_audio.assert_called_once_with("/test.mp3", blocking=False) + camera[0].wait_until_audio_completes.assert_called_once() + + +async def test_media_player_play_invalid( + hass: HomeAssistant, + camera: tuple[Camera, str], +): + """Test media_player entity test play_media, not music.""" + + camera[0].__fields__["play_audio"] = Mock() + camera[0].play_audio = AsyncMock() + + await hass.services.async_call( + "media_player", + "play_media", + { + ATTR_ENTITY_ID: camera[1], + "media_content_id": "/test.png", + "media_content_type": "image", + }, + blocking=True, + ) + + assert not camera[0].play_audio.called + + +async def test_media_player_play_error( + hass: HomeAssistant, + camera: tuple[Camera, str], +): + """Test media_player entity test play_media, not music.""" + + camera[0].__fields__["play_audio"] = Mock() + camera[0].__fields__["wait_until_audio_completes"] = Mock() + camera[0].play_audio = AsyncMock(side_effect=StreamError) + camera[0].wait_until_audio_completes = AsyncMock() + + await hass.services.async_call( + "media_player", + "play_media", + { + ATTR_ENTITY_ID: camera[1], + "media_content_id": "/test.mp3", + "media_content_type": "music", + }, + blocking=True, + ) + + assert camera[0].play_audio.called + assert not camera[0].wait_until_audio_completes.called