"""Support for interacting with and controlling the cmus music player.""" from __future__ import annotations import logging from typing import Any from pycmus import exceptions, remote import voluptuous as vol from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, MediaType, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "cmus" DEFAULT_PORT = 3000 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Inclusive(CONF_HOST, "remote"): cv.string, vol.Inclusive(CONF_PASSWORD, "remote"): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, } ) def setup_platform( hass: HomeAssistant, config: ConfigType, add_entities: AddEntitiesCallback, discover_info: DiscoveryInfoType | None = None, ) -> None: """Set up the CMUS platform.""" host = config.get(CONF_HOST) password = config.get(CONF_PASSWORD) port = config[CONF_PORT] name = config[CONF_NAME] cmus_remote = CmusRemote(server=host, port=port, password=password) cmus_remote.connect() if cmus_remote.cmus is None: return add_entities([CmusDevice(device=cmus_remote, name=name, server=host)], True) class CmusRemote: """Representation of a cmus connection.""" def __init__(self, server, port, password): """Initialize the cmus remote.""" self._server = server self._port = port self._password = password self.cmus = None def connect(self): """Connect to the cmus server.""" try: self.cmus = remote.PyCmus( server=self._server, port=self._port, password=self._password ) except exceptions.InvalidPassword: _LOGGER.error("The provided password was rejected by cmus") class CmusDevice(MediaPlayerEntity): """Representation of a running cmus.""" _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.PREVIOUS_TRACK | MediaPlayerEntityFeature.NEXT_TRACK | MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.SEEK | MediaPlayerEntityFeature.PLAY ) def __init__(self, device, name, server): """Initialize the CMUS device.""" self._remote = device if server: auto_name = f"cmus-{server}" else: auto_name = "cmus-local" self._attr_name = name or auto_name self.status = {} def update(self) -> None: """Get the latest data and update the state.""" try: status = self._remote.cmus.get_status_dict() except BrokenPipeError: self._remote.connect() except exceptions.ConfigurationError: _LOGGER.warning("A configuration error occurred") self._remote.connect() else: self.status = status if self.status.get("status") == "playing": self._attr_state = MediaPlayerState.PLAYING elif self.status.get("status") == "paused": self._attr_state = MediaPlayerState.PAUSED else: self._attr_state = MediaPlayerState.OFF self._attr_media_content_id = self.status.get("file") self._attr_media_duration = self.status.get("duration") self._attr_media_title = self.status["tag"].get("title") self._attr_media_artist = self.status["tag"].get("artist") self._attr_media_track = self.status["tag"].get("tracknumber") self._attr_media_album_name = self.status["tag"].get("album") self._attr_media_album_artist = self.status["tag"].get("albumartist") left = self.status["set"].get("vol_left")[0] right = self.status["set"].get("vol_right")[0] if left != right: volume = float(left + right) / 2 else: volume = left self._attr_volume_level = int(volume) / 100 return _LOGGER.warning("Received no status from cmus") def turn_off(self) -> None: """Service to send the CMUS the command to stop playing.""" self._remote.cmus.player_stop() def turn_on(self) -> None: """Service to send the CMUS the command to start playing.""" self._remote.cmus.player_play() def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._remote.cmus.set_volume(int(volume * 100)) def volume_up(self) -> None: """Set the volume up.""" left = self.status["set"].get("vol_left") right = self.status["set"].get("vol_right") if left != right: current_volume = float(left + right) / 2 else: current_volume = left if current_volume <= 100: self._remote.cmus.set_volume(int(current_volume) + 5) def volume_down(self) -> None: """Set the volume down.""" left = self.status["set"].get("vol_left") right = self.status["set"].get("vol_right") if left != right: current_volume = float(left + right) / 2 else: current_volume = left if current_volume <= 100: self._remote.cmus.set_volume(int(current_volume) - 5) def play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Send the play command.""" if media_type in {MediaType.MUSIC, MediaType.PLAYLIST}: self._remote.cmus.player_play_file(media_id) else: _LOGGER.error( "Invalid media type %s. Only %s and %s are supported", media_type, MediaType.MUSIC, MediaType.PLAYLIST, ) def media_pause(self) -> None: """Send the pause command.""" self._remote.cmus.player_pause() def media_next_track(self) -> None: """Send next track command.""" self._remote.cmus.player_next() def media_previous_track(self) -> None: """Send next track command.""" self._remote.cmus.player_prev() def media_seek(self, position: float) -> None: """Send seek command.""" self._remote.cmus.seek(position) def media_play(self) -> None: """Send the play command.""" self._remote.cmus.player_play() def media_stop(self) -> None: """Send the stop command.""" self._remote.cmus.stop()