mirror of https://github.com/home-assistant/core
245 lines
9.1 KiB
Python
245 lines
9.1 KiB
Python
"""MediaPlayer platform for SlimProto Player integration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from typing import Any
|
|
|
|
from aioslimproto.client import PlayerState, SlimClient
|
|
from aioslimproto.models import EventType, SlimEvent
|
|
from aioslimproto.server import SlimServer
|
|
|
|
from homeassistant.components import media_source
|
|
from homeassistant.components.media_player import (
|
|
BrowseMedia,
|
|
MediaPlayerDeviceClass,
|
|
MediaPlayerEntity,
|
|
MediaPlayerEntityFeature,
|
|
MediaPlayerState,
|
|
MediaType,
|
|
async_process_play_media_url,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.helpers.device_registry import DeviceInfo
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
from homeassistant.util.dt import utcnow
|
|
|
|
from .const import DEFAULT_NAME, DOMAIN, PLAYER_EVENT
|
|
|
|
STATE_MAPPING = {
|
|
PlayerState.STOPPED: MediaPlayerState.IDLE,
|
|
PlayerState.PLAYING: MediaPlayerState.PLAYING,
|
|
PlayerState.BUFFER_READY: MediaPlayerState.PLAYING,
|
|
PlayerState.BUFFERING: MediaPlayerState.PLAYING,
|
|
PlayerState.PAUSED: MediaPlayerState.PAUSED,
|
|
}
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up SlimProto MediaPlayer(s) from Config Entry."""
|
|
slimserver: SlimServer = hass.data[DOMAIN]
|
|
added_ids = set()
|
|
|
|
async def async_add_player(player: SlimClient) -> None:
|
|
"""Add MediaPlayerEntity from SlimClient."""
|
|
# we delay adding the player a small bit because the player name may be received
|
|
# just a bit after connect. This way we can create a device reg entry with the
|
|
# correct name the name will either be available within a few milliseconds after
|
|
# connect or not at all (its an optional data packet)
|
|
for _ in range(10):
|
|
if player.player_id not in player.name:
|
|
break
|
|
await asyncio.sleep(0.1)
|
|
async_add_entities([SlimProtoPlayer(slimserver, player)])
|
|
|
|
async def on_slim_event(event: SlimEvent) -> None:
|
|
"""Handle player added/connected event."""
|
|
if event.player_id in added_ids:
|
|
return
|
|
added_ids.add(event.player_id)
|
|
player = slimserver.get_player(event.player_id)
|
|
await async_add_player(player)
|
|
|
|
# register listener for new players
|
|
config_entry.async_on_unload(
|
|
slimserver.subscribe(on_slim_event, EventType.PLAYER_CONNECTED)
|
|
)
|
|
|
|
# add all current items in controller
|
|
await asyncio.gather(*(async_add_player(player) for player in slimserver.players))
|
|
|
|
|
|
class SlimProtoPlayer(MediaPlayerEntity):
|
|
"""Representation of MediaPlayerEntity from SlimProto Player."""
|
|
|
|
_attr_has_entity_name = True
|
|
_attr_should_poll = False
|
|
_attr_supported_features = (
|
|
MediaPlayerEntityFeature.PAUSE
|
|
| MediaPlayerEntityFeature.VOLUME_SET
|
|
| MediaPlayerEntityFeature.STOP
|
|
| MediaPlayerEntityFeature.TURN_ON
|
|
| MediaPlayerEntityFeature.TURN_OFF
|
|
| MediaPlayerEntityFeature.PLAY
|
|
| MediaPlayerEntityFeature.PLAY_MEDIA
|
|
| MediaPlayerEntityFeature.VOLUME_MUTE
|
|
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
|
)
|
|
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
|
_attr_name = None
|
|
|
|
def __init__(self, slimserver: SlimServer, player: SlimClient) -> None:
|
|
"""Initialize MediaPlayer entity."""
|
|
self.slimserver = slimserver
|
|
self.player = player
|
|
self._attr_unique_id = player.player_id
|
|
self._attr_device_info = DeviceInfo(
|
|
identifiers={(DOMAIN, self.player.player_id)},
|
|
manufacturer=DEFAULT_NAME,
|
|
model=self.player.device_model or self.player.device_type,
|
|
name=self.player.name,
|
|
hw_version=self.player.firmware,
|
|
)
|
|
# PiCore + SqueezeESP32 player has web interface
|
|
if "-pCP" in self.player.firmware or self.player.device_model == "SqueezeESP32":
|
|
self._attr_device_info["configuration_url"] = (
|
|
f"http://{self.player.device_address}"
|
|
)
|
|
self.update_attributes()
|
|
|
|
async def async_added_to_hass(self) -> None:
|
|
"""Register callbacks."""
|
|
self.update_attributes()
|
|
self.async_on_remove(
|
|
self.slimserver.subscribe(
|
|
self._on_slim_event,
|
|
(
|
|
EventType.PLAYER_UPDATED,
|
|
EventType.PLAYER_CONNECTED,
|
|
EventType.PLAYER_DISCONNECTED,
|
|
EventType.PLAYER_NAME_RECEIVED,
|
|
EventType.PLAYER_CLI_EVENT,
|
|
),
|
|
player_filter=self.player.player_id,
|
|
)
|
|
)
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""Return availability of entity."""
|
|
return self.player.connected
|
|
|
|
@property
|
|
def state(self) -> MediaPlayerState:
|
|
"""Return current state."""
|
|
if not self.player.powered:
|
|
return MediaPlayerState.OFF
|
|
return STATE_MAPPING[self.player.state]
|
|
|
|
@callback
|
|
def update_attributes(self) -> None:
|
|
"""Handle player updates."""
|
|
self._attr_volume_level = self.player.volume_level / 100
|
|
self._attr_is_volume_muted = self.player.muted
|
|
self._attr_media_position = self.player.elapsed_seconds
|
|
self._attr_media_position_updated_at = utcnow()
|
|
if (current_media := self.player.current_media) and (
|
|
metadata := current_media.metadata
|
|
):
|
|
self._attr_media_content_id = metadata.get("item_id", current_media.url)
|
|
self._attr_media_artist = metadata.get("artist")
|
|
self._attr_media_album_name = metadata.get("album")
|
|
self._attr_media_title = metadata.get("title")
|
|
self._attr_media_image_url = metadata.get("image_url")
|
|
else:
|
|
self._attr_media_content_id = current_media.url if current_media else None
|
|
self._attr_media_artist = None
|
|
self._attr_media_album_name = None
|
|
self._attr_media_title = None
|
|
self._attr_media_image_url = None
|
|
self._attr_media_content_type = "music"
|
|
|
|
async def async_media_play(self) -> None:
|
|
"""Send play command to device."""
|
|
await self.player.play()
|
|
|
|
async def async_media_pause(self) -> None:
|
|
"""Send pause command to device."""
|
|
await self.player.pause()
|
|
|
|
async def async_media_stop(self) -> None:
|
|
"""Send stop command to device."""
|
|
await self.player.stop()
|
|
|
|
async def async_set_volume_level(self, volume: float) -> None:
|
|
"""Send new volume_level to device."""
|
|
volume = round(volume * 100)
|
|
await self.player.volume_set(volume)
|
|
|
|
async def async_mute_volume(self, mute: bool) -> None:
|
|
"""Mute the volume."""
|
|
await self.player.mute(mute)
|
|
|
|
async def async_turn_on(self) -> None:
|
|
"""Turn on device."""
|
|
await self.player.power(True)
|
|
|
|
async def async_turn_off(self) -> None:
|
|
"""Turn off device."""
|
|
await self.player.power(False)
|
|
|
|
async def async_play_media(
|
|
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
|
) -> None:
|
|
"""Send the play_media command to the media player."""
|
|
to_send_media_type: str | None = media_type
|
|
# Handle media_source
|
|
if media_source.is_media_source_id(media_id):
|
|
sourced_media = await media_source.async_resolve_media(
|
|
self.hass, media_id, self.entity_id
|
|
)
|
|
media_id = sourced_media.url
|
|
to_send_media_type = sourced_media.mime_type
|
|
|
|
if to_send_media_type and not to_send_media_type.startswith("audio/"):
|
|
to_send_media_type = None
|
|
media_id = async_process_play_media_url(self.hass, media_id)
|
|
|
|
await self.player.play_url(media_id, mime_type=to_send_media_type)
|
|
|
|
async def async_browse_media(
|
|
self,
|
|
media_content_type: MediaType | 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/"),
|
|
)
|
|
|
|
async def _on_slim_event(self, event: SlimEvent) -> None:
|
|
"""Call when we receive an event from SlimProto."""
|
|
if event.type == EventType.PLAYER_CONNECTED:
|
|
# player reconnected, update our player object
|
|
self.player = self.slimserver.get_player(event.player_id)
|
|
if event.type == EventType.PLAYER_CLI_EVENT:
|
|
# rpc event from player such as a button press,
|
|
# forward on the eventbus for others to handle
|
|
dev_id = self.registry_entry.device_id if self.registry_entry else None
|
|
evt_data = {
|
|
**event.data,
|
|
"entity_id": self.entity_id,
|
|
"device_id": dev_id,
|
|
}
|
|
self.hass.bus.async_fire(PLAYER_EVENT, evt_data)
|
|
return
|
|
self.update_attributes()
|
|
self.async_write_ha_state()
|