diff --git a/CODEOWNERS b/CODEOWNERS index 815f1b6b85a..f60da8494d7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -363,6 +363,7 @@ homeassistant/components/speedtestdotnet/* @rohankapoorcom homeassistant/components/spider/* @peternijssen homeassistant/components/spotify/* @frenck homeassistant/components/sql/* @dgomes +homeassistant/components/squeezebox/* @rajlaud homeassistant/components/starline/* @anonym-tsk homeassistant/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm diff --git a/homeassistant/components/squeezebox/const.py b/homeassistant/components/squeezebox/const.py index 1e8fd6f3a2a..e7e52fe2d80 100644 --- a/homeassistant/components/squeezebox/const.py +++ b/homeassistant/components/squeezebox/const.py @@ -1,3 +1,10 @@ """Constants for the Squeezebox component.""" +from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING + DOMAIN = "squeezebox" SERVICE_CALL_METHOD = "call_method" +SQUEEZEBOX_MODE = { + "pause": STATE_PAUSED, + "play": STATE_PLAYING, + "stop": STATE_IDLE, +} diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index bbd32e9eefe..13c200fc46f 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -2,5 +2,6 @@ "domain": "squeezebox", "name": "Logitech Squeezebox", "documentation": "https://www.home-assistant.io/integrations/squeezebox", - "codeowners": [] + "codeowners": ["@rajlaud"], + "requirements": ["pysqueezebox==0.1.2"] } diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index f40374d9486..8b27defd5e1 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -1,12 +1,9 @@ """Support for interfacing to the Logitech SqueezeBox API.""" import asyncio -import json import logging import socket -import urllib.parse -import aiohttp -import async_timeout +from pysqueezebox import Server import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity @@ -33,18 +30,14 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - HTTP_OK, - STATE_IDLE, STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -from .const import DOMAIN, SERVICE_CALL_METHOD +from .const import DOMAIN, SERVICE_CALL_METHOD, SQUEEZEBOX_MODE _LOGGER = logging.getLogger(__name__) @@ -126,7 +119,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Get IP of host, to prevent duplication of same host (different DNS names) try: - ipaddr = socket.gethostbyname(host) + ipaddr = await hass.async_add_executor_job(socket.gethostbyname, host) except OSError as error: _LOGGER.error("Could not communicate with %s:%d: %s", host, port, error) raise PlatformNotReady from error @@ -135,16 +128,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return _LOGGER.debug("Creating LMS object for %s", ipaddr) - lms = LogitechMediaServer(hass, host, port, username, password) - - players = await lms.create_players() - if players is None: - raise PlatformNotReady - + lms = Server(async_get_clientsession(hass), host, port, username, password) known_servers.add(ipaddr) - hass.data[DATA_SQUEEZEBOX].extend(players) - async_add_entities(players) + players = await lms.async_get_players() + if players is None: + raise PlatformNotReady + media_players = [] + for player in players: + media_players.append(SqueezeBoxDevice(player)) + + hass.data[DATA_SQUEEZEBOX].extend(media_players) + async_add_entities(media_players) async def async_service_handler(service): """Map services to methods on MediaPlayerEntity.""" @@ -182,133 +177,41 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return True -class LogitechMediaServer: - """Representation of a Logitech media server.""" - - def __init__(self, hass, host, port, username, password): - """Initialize the Logitech device.""" - self.hass = hass - self.host = host - self.port = port - self._username = username - self._password = password - - async def create_players(self): - """Create a list of devices connected to LMS.""" - result = [] - data = await self.async_query("players", "status") - if data is False: - return None - for players in data.get("players_loop", []): - player = SqueezeBoxDevice(self, players["playerid"], players["name"]) - await player.async_update() - result.append(player) - return result - - async def async_query(self, *command, player=""): - """Abstract out the JSON-RPC connection.""" - auth = ( - None - if self._username is None - else aiohttp.BasicAuth(self._username, self._password) - ) - url = f"http://{self.host}:{self.port}/jsonrpc.js" - data = json.dumps( - {"id": "1", "method": "slim.request", "params": [player, command]} - ) - - _LOGGER.debug("URL: %s Data: %s", url, data) - - try: - websession = async_get_clientsession(self.hass) - with async_timeout.timeout(TIMEOUT): - response = await websession.post(url, data=data, auth=auth) - - if response.status != HTTP_OK: - _LOGGER.error( - "Query failed, response code: %s Full message: %s", - response.status, - response, - ) - return False - - data = await response.json() - - except (asyncio.TimeoutError, aiohttp.ClientError) as error: - _LOGGER.error("Failed communicating with LMS: %s", type(error)) - return False - - try: - return data["result"] - except AttributeError: - _LOGGER.error("Received invalid response: %s", data) - return False - - class SqueezeBoxDevice(MediaPlayerEntity): - """Representation of a SqueezeBox device.""" + """ + Representation of a SqueezeBox device. - def __init__(self, lms, player_id, name): + Wraps a pysqueezebox.Player() object. + """ + + def __init__(self, player): """Initialize the SqueezeBox device.""" - super().__init__() - self._lms = lms - self._id = player_id - self._status = {} - self._name = name + self._player = player self._last_update = None - _LOGGER.debug("Creating SqueezeBox object: %s, %s", name, player_id) @property def name(self): """Return the name of the device.""" - return self._name + return self._player.name @property def unique_id(self): """Return a unique ID.""" - return self._id + return self._player.player_id @property def state(self): """Return the state of the device.""" - if "power" in self._status and self._status["power"] == 0: + if self._player.power is not None and not self._player.power: return STATE_OFF - if "mode" in self._status: - if self._status["mode"] == "pause": - return STATE_PAUSED - if self._status["mode"] == "play": - return STATE_PLAYING - if self._status["mode"] == "stop": - return STATE_IDLE + if self._player.mode: + return SQUEEZEBOX_MODE.get(self._player.mode) return None - async def async_query(self, *parameters): - """Send a command to the LMS.""" - return await self._lms.async_query(*parameters, player=self._id) - async def async_update(self): - """Retrieve the current state of the player.""" - tags = "adKl" - response = await self.async_query("status", "-", "1", f"tags:{tags}") - - if response is False: - return - + """Update the Player() object.""" last_media_position = self.media_position - - self._status = {} - - try: - self._status.update(response["playlist_loop"][0]) - except KeyError: - pass - try: - self._status.update(response["remoteMeta"]) - except KeyError: - pass - - self._status.update(response) - + await self._player.async_update() if self.media_position != last_media_position: _LOGGER.debug( "Media position updated for %s: %s", self, self.media_position @@ -318,20 +221,18 @@ class SqueezeBoxDevice(MediaPlayerEntity): @property def volume_level(self): """Volume level of the media player (0..1).""" - if "mixer volume" in self._status: - return int(float(self._status["mixer volume"])) / 100.0 + if self._player.volume: + return int(float(self._player.volume)) / 100.0 @property def is_volume_muted(self): """Return true if volume is muted.""" - if "mixer volume" in self._status: - return str(self._status["mixer volume"]).startswith("-") + return self._player.muting @property def media_content_id(self): """Content ID of current playing media.""" - if "current_title" in self._status: - return self._status["current_title"] + return self._player.url @property def media_content_type(self): @@ -341,14 +242,12 @@ class SqueezeBoxDevice(MediaPlayerEntity): @property def media_duration(self): """Duration of current playing media in seconds.""" - if "duration" in self._status: - return int(float(self._status["duration"])) + return self._player.duration @property def media_position(self): - """Duration of current playing media in seconds.""" - if "time" in self._status: - return int(float(self._status["time"])) + """Position of current playing media in seconds.""" + return self._player.time @property def media_position_updated_at(self): @@ -358,60 +257,27 @@ class SqueezeBoxDevice(MediaPlayerEntity): @property def media_image_url(self): """Image url of current playing media.""" - if "artwork_url" in self._status: - media_url = self._status["artwork_url"] - elif "id" in self._status: - media_url = ("/music/{track_id}/cover.jpg").format( - track_id=self._status["id"] - ) - else: - media_url = ("/music/current/cover.jpg?player={player}").format( - player=self._id - ) - - # pylint: disable=protected-access - if self._lms._username: - base_url = "http://{username}:{password}@{server}:{port}/".format( - username=self._lms._username, - password=self._lms._password, - server=self._lms.host, - port=self._lms.port, - ) - else: - base_url = "http://{server}:{port}/".format( - server=self._lms.host, port=self._lms.port - ) - - url = urllib.parse.urljoin(base_url, media_url) - - return url + return self._player.image_url @property def media_title(self): """Title of current playing media.""" - if "title" in self._status: - return self._status["title"] - - if "current_title" in self._status: - return self._status["current_title"] + return self._player.title @property def media_artist(self): """Artist of current playing media.""" - if "artist" in self._status: - return self._status["artist"] + return self._player.artist @property def media_album_name(self): """Album of current playing media.""" - if "album" in self._status: - return self._status["album"] + return self._player.album @property def shuffle(self): """Boolean if shuffle is enabled.""" - if "playlist_shuffle" in self._status: - return self._status["playlist_shuffle"] == 1 + return self._player.shuffle @property def supported_features(self): @@ -420,53 +286,52 @@ class SqueezeBoxDevice(MediaPlayerEntity): async def async_turn_off(self): """Turn off media player.""" - await self.async_query("power", "0") + await self._player.async_set_power(False) async def async_volume_up(self): """Volume up media player.""" - await self.async_query("mixer", "volume", "+5") + await self._player.async_set_volume("+5") async def async_volume_down(self): """Volume down media player.""" - await self.async_query("mixer", "volume", "-5") + await self._player.async_set_volume("-5") async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" volume_percent = str(int(volume * 100)) - await self.async_query("mixer", "volume", volume_percent) + await self._player.async_set_volume(volume_percent) async def async_mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" - mute_numeric = "1" if mute else "0" - await self.async_query("mixer", "muting", mute_numeric) + await self._player.async_set_muting(mute) async def async_media_play_pause(self): """Send pause command to media player.""" - await self.async_query("pause") + await self._player.async_toggle_pause() async def async_media_play(self): """Send play command to media player.""" - await self.async_query("play") + await self._player.async_play() async def async_media_pause(self): """Send pause command to media player.""" - await self.async_query("pause", "1") + await self._player.async_pause() async def async_media_next_track(self): """Send next track command.""" - await self.async_query("playlist", "index", "+1") + await self._player.async_index("+1") async def async_media_previous_track(self): """Send next track command.""" - await self.async_query("playlist", "index", "-1") + await self._player.async_index("-1") async def async_media_seek(self, position): """Send seek command.""" - await self.async_query("time", position) + await self._player.async_time(position) async def async_turn_on(self): """Turn the media player on.""" - await self.async_query("power", "1") + await self._player.async_set_power(True) async def async_play_media(self, media_type, media_id, **kwargs): """ @@ -474,27 +339,20 @@ class SqueezeBoxDevice(MediaPlayerEntity): If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the current playlist. """ + cmd = "play" if kwargs.get(ATTR_MEDIA_ENQUEUE): - await self._add_uri_to_playlist(media_id) - return + cmd = "add" - await self._play_uri(media_id) - - async def _play_uri(self, media_id): - """Replace the current play list with the uri.""" - await self.async_query("playlist", "play", media_id) - - async def _add_uri_to_playlist(self, media_id): - """Add an item to the existing playlist.""" - await self.async_query("playlist", "add", media_id) + await self._player.async_load_url(media_id, cmd) async def async_set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" - await self.async_query("playlist", "shuffle", int(shuffle)) + shuffle_mode = "song" if shuffle else "none" + await self._player.async_set_shuffle(shuffle_mode) async def async_clear_playlist(self): """Send the media player the command for clear playlist.""" - await self.async_query("playlist", "clear") + await self._player.async_clear_playlist() async def async_call_method(self, command, parameters=None): """ @@ -507,4 +365,4 @@ class SqueezeBoxDevice(MediaPlayerEntity): if parameters: for parameter in parameters: all_params.append(parameter) - await self.async_query(*all_params) + await self._player.async_query(*all_params) diff --git a/requirements_all.txt b/requirements_all.txt index 724f59f874d..10b9251de24 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1580,6 +1580,9 @@ pysonos==0.0.25 # homeassistant.components.spc pyspcwebgw==0.4.0 +# homeassistant.components.squeezebox +pysqueezebox==0.1.2 + # homeassistant.components.stiebel_eltron pystiebeleltron==0.0.1.dev2