diff --git a/homeassistant/components/camera/media_source.py b/homeassistant/components/camera/media_source.py index cf896ee0456..4bb6ed5f921 100644 --- a/homeassistant/components/camera/media_source.py +++ b/homeassistant/components/camera/media_source.py @@ -13,6 +13,7 @@ from homeassistant.components.media_source.models import ( PlayMedia, ) from homeassistant.components.stream import FORMAT_CONTENT_TYPE, HLS_PROVIDER +from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_component import EntityComponent @@ -26,13 +27,20 @@ async def async_get_media_source(hass: HomeAssistant) -> CameraMediaSource: return CameraMediaSource(hass) -def _media_source_for_camera(camera: Camera, content_type: str) -> BrowseMediaSource: +def _media_source_for_camera( + hass: HomeAssistant, camera: Camera, content_type: str +) -> BrowseMediaSource: + camera_state = hass.states.get(camera.entity_id) + title = camera.name + if camera_state: + title = camera_state.attributes.get(ATTR_FRIENDLY_NAME, camera.name) + return BrowseMediaSource( domain=DOMAIN, identifier=camera.entity_id, media_class=MediaClass.VIDEO, media_content_type=content_type, - title=camera.name, + title=title, thumbnail=f"/api/camera_proxy/{camera.entity_id}", can_play=True, can_expand=False, @@ -90,7 +98,7 @@ class CameraMediaSource(MediaSource): async def _filter_browsable_camera(camera: Camera) -> BrowseMediaSource | None: stream_type = camera.frontend_stream_type if stream_type is None: - return _media_source_for_camera(camera, camera.content_type) + return _media_source_for_camera(self.hass, camera, camera.content_type) if not can_stream_hls: return None @@ -98,7 +106,7 @@ class CameraMediaSource(MediaSource): if stream_type != StreamType.HLS and not (await camera.stream_source()): return None - return _media_source_for_camera(camera, content_type) + return _media_source_for_camera(self.hass, camera, content_type) component: EntityComponent[Camera] = self.hass.data[DOMAIN] results = await asyncio.gather( diff --git a/tests/components/camera/conftest.py b/tests/components/camera/conftest.py index 37c3ba8057c..c1d630de126 100644 --- a/tests/components/camera/conftest.py +++ b/tests/components/camera/conftest.py @@ -8,6 +8,7 @@ from homeassistant.components import camera from homeassistant.components.camera.const import StreamType from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.setup import async_setup_component from .common import WEBRTC_ANSWER @@ -70,3 +71,37 @@ async def mock_camera_web_rtc_fixture(hass): return_value=WEBRTC_ANSWER, ): yield + + +@pytest.fixture(name="mock_camera_with_device") +async def mock_camera_with_device_fixture(): + """Initialize a demo camera platform with a device.""" + dev_info = DeviceInfo( + identifiers={("camera", "test_unique_id")}, + name="Test Camera Device", + ) + + class UniqueIdMock(PropertyMock): + def __get__(self, obj, obj_type=None): + return obj.name + + with patch( + "homeassistant.components.camera.Camera.has_entity_name", + new_callable=PropertyMock(return_value=True), + ), patch( + "homeassistant.components.camera.Camera.unique_id", new=UniqueIdMock() + ), patch( + "homeassistant.components.camera.Camera.device_info", + new_callable=PropertyMock(return_value=dev_info), + ): + yield + + +@pytest.fixture(name="mock_camera_with_no_name") +async def mock_camera_with_no_name_fixture(mock_camera_with_device): + """Initialize a demo camera platform with a device and no name.""" + with patch( + "homeassistant.components.camera.Camera._attr_name", + new_callable=PropertyMock(return_value=None), + ): + yield diff --git a/tests/components/camera/test_media_source.py b/tests/components/camera/test_media_source.py index 5e8fbbd1fb2..bab6d3a236b 100644 --- a/tests/components/camera/test_media_source.py +++ b/tests/components/camera/test_media_source.py @@ -17,6 +17,26 @@ async def setup_media_source(hass): assert await async_setup_component(hass, "media_source", {}) +async def test_device_with_device( + hass: HomeAssistant, mock_camera_with_device, mock_camera +) -> None: + """Test browsing when camera has a device and a name.""" + item = await media_source.async_browse_media(hass, "media-source://camera") + assert item.not_shown == 2 + assert len(item.children) == 1 + assert item.children[0].title == "Test Camera Device Demo camera without stream" + + +async def test_device_with_no_name( + hass: HomeAssistant, mock_camera_with_no_name, mock_camera +) -> None: + """Test browsing when camera has device and name == None.""" + item = await media_source.async_browse_media(hass, "media-source://camera") + assert item.not_shown == 2 + assert len(item.children) == 1 + assert item.children[0].title == "Test Camera Device Demo camera without stream" + + async def test_browsing_hls(hass: HomeAssistant, mock_camera_hls) -> None: """Test browsing HLS camera media source.""" item = await media_source.async_browse_media(hass, "media-source://camera") @@ -42,6 +62,7 @@ async def test_browsing_mjpeg(hass: HomeAssistant, mock_camera) -> None: assert len(item.children) == 1 assert item.not_shown == 2 assert item.children[0].media_content_type == "image/jpg" + assert item.children[0].title == "Demo camera without stream" async def test_browsing_web_rtc(hass: HomeAssistant, mock_camera_web_rtc) -> None: