diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index ebd9af1134a..86ab769e0ec 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -9,6 +9,7 @@ from .coordinator import FullyKioskDataUpdateCoordinator PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.MEDIA_PLAYER, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/components/fully_kiosk/const.py b/homeassistant/components/fully_kiosk/const.py index f21906bae73..56248544b81 100644 --- a/homeassistant/components/fully_kiosk/const.py +++ b/homeassistant/components/fully_kiosk/const.py @@ -5,9 +5,20 @@ from datetime import timedelta import logging from typing import Final +from homeassistant.components.media_player.const import MediaPlayerEntityFeature + DOMAIN: Final = "fully_kiosk" LOGGER = logging.getLogger(__package__) UPDATE_INTERVAL = timedelta(seconds=30) DEFAULT_PORT = 2323 + +AUDIOMANAGER_STREAM_MUSIC = 3 + +MEDIA_SUPPORT_FULLYKIOSK = ( + MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.BROWSE_MEDIA +) diff --git a/homeassistant/components/fully_kiosk/media_player.py b/homeassistant/components/fully_kiosk/media_player.py new file mode 100644 index 00000000000..732f88170e1 --- /dev/null +++ b/homeassistant/components/fully_kiosk/media_player.py @@ -0,0 +1,82 @@ +"""Fully Kiosk Browser media player.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components import media_source +from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + BrowseMedia, + async_process_play_media_url, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_IDLE, STATE_PLAYING +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import AUDIOMANAGER_STREAM_MUSIC, DOMAIN, MEDIA_SUPPORT_FULLYKIOSK +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Fully Kiosk Browser media player entity.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities([FullyMediaPlayer(coordinator)]) + + +class FullyMediaPlayer(FullyKioskEntity, MediaPlayerEntity): + """Representation of a Fully Kiosk Browser media player entity.""" + + _attr_supported_features = MEDIA_SUPPORT_FULLYKIOSK + _attr_assumed_state = True + _attr_state = STATE_IDLE + + def __init__(self, coordinator: FullyKioskDataUpdateCoordinator) -> None: + """Initialize the media player entity.""" + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.data['deviceID']}-mediaplayer" + + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: + """Play a piece of media.""" + if media_source.is_media_source_id(media_id): + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) + media_id = async_process_play_media_url(self.hass, play_item.url) + + await self.coordinator.fully.playSound(media_id, AUDIOMANAGER_STREAM_MUSIC) + self._attr_state = STATE_PLAYING + self.async_write_ha_state() + + async def async_media_stop(self) -> None: + """Stop playing media.""" + await self.coordinator.fully.stopSound() + self._attr_state = STATE_IDLE + self.async_write_ha_state() + + async def async_set_volume_level(self, volume: float) -> None: + """Set volume level, range 0..1.""" + await self.coordinator.fully.setAudioVolume( + int(volume * 100), AUDIOMANAGER_STREAM_MUSIC + ) + self._attr_volume_level = volume + self.async_write_ha_state() + + async def async_browse_media( + self, + media_content_type: str | None = None, + media_content_id: str | None = None, + ) -> BrowseMedia: + """Implement the WebSocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) diff --git a/tests/components/fully_kiosk/test_media_player.py b/tests/components/fully_kiosk/test_media_player.py new file mode 100644 index 00000000000..d423d809fbe --- /dev/null +++ b/tests/components/fully_kiosk/test_media_player.py @@ -0,0 +1,132 @@ +"""Test the Fully Kiosk Browser media player.""" +from unittest.mock import MagicMock, Mock, patch + +from aiohttp import ClientSession + +from homeassistant.components.fully_kiosk.const import DOMAIN, MEDIA_SUPPORT_FULLYKIOSK +import homeassistant.components.media_player as media_player +from homeassistant.components.media_source.const import DOMAIN as MS_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_media_player( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test standard Fully Kiosk media player.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("media_player.amazon_fire") + assert state + + entry = entity_registry.async_get("media_player.amazon_fire") + assert entry + assert entry.unique_id == "abcdef-123456-mediaplayer" + assert entry.supported_features == MEDIA_SUPPORT_FULLYKIOSK + + await hass.services.async_call( + media_player.DOMAIN, + "play_media", + { + ATTR_ENTITY_ID: "media_player.amazon_fire", + "media_content_type": "music", + "media_content_id": "test.mp3", + }, + blocking=True, + ) + assert len(mock_fully_kiosk.playSound.mock_calls) == 1 + + with patch( + "homeassistant.components.media_source.async_resolve_media", + return_value=Mock(url="http://example.com/test.mp3"), + ): + await hass.services.async_call( + "media_player", + "play_media", + { + ATTR_ENTITY_ID: "media_player.amazon_fire", + "media_content_id": "media-source://some_source/some_id", + "media_content_type": "audio/mpeg", + }, + blocking=True, + ) + + assert len(mock_fully_kiosk.playSound.mock_calls) == 2 + assert ( + mock_fully_kiosk.playSound.mock_calls[1].args[0] + == "http://example.com/test.mp3" + ) + + await hass.services.async_call( + media_player.DOMAIN, + "media_stop", + { + ATTR_ENTITY_ID: "media_player.amazon_fire", + }, + blocking=True, + ) + assert len(mock_fully_kiosk.stopSound.mock_calls) == 1 + + await hass.services.async_call( + media_player.DOMAIN, + "volume_set", + { + ATTR_ENTITY_ID: "media_player.amazon_fire", + "volume_level": 0.5, + }, + blocking=True, + ) + assert len(mock_fully_kiosk.setAudioVolume.mock_calls) == 1 + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url == "http://192.168.1.234:2323" + assert device_entry.entry_type is None + assert device_entry.hw_version is None + assert device_entry.identifiers == {(DOMAIN, "abcdef-123456")} + assert device_entry.manufacturer == "amzn" + assert device_entry.model == "KFDOWI" + assert device_entry.name == "Amazon Fire" + assert device_entry.sw_version == "1.42.5" + + +async def test_browse_media( + hass: HomeAssistant, + hass_ws_client: ClientSession, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test Fully Kiosk browse media.""" + + await async_setup_component(hass, MS_DOMAIN, {MS_DOMAIN: {}}) + await hass.async_block_till_done() + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": "media_player.amazon_fire", + } + ) + response = await client.receive_json() + assert response["success"] + expected_child_audio = { + "title": "test.mp3", + "media_class": "music", + "media_content_type": "audio/mpeg", + "media_content_id": "media-source://media_source/local/test.mp3", + "can_play": True, + "can_expand": False, + "thumbnail": None, + "children_media_class": None, + } + assert expected_child_audio in response["result"]["children"]