Add UniFi Protect media_player (#62895)

This commit is contained in:
Christopher Bailey 2021-12-28 23:36:18 -05:00 committed by GitHub
parent 4025ce8f97
commit 490d76e01e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 470 additions and 83 deletions

View file

@ -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]

View file

@ -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",

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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"

View file

@ -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