Refactor squeezebox (#34731)

Co-Authored-By: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
rajlaud 2020-04-27 05:15:00 -05:00 committed by GitHub
parent be57d45653
commit 5e249aac5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 71 additions and 201 deletions

View File

@ -363,6 +363,7 @@ homeassistant/components/speedtestdotnet/* @rohankapoorcom
homeassistant/components/spider/* @peternijssen homeassistant/components/spider/* @peternijssen
homeassistant/components/spotify/* @frenck homeassistant/components/spotify/* @frenck
homeassistant/components/sql/* @dgomes homeassistant/components/sql/* @dgomes
homeassistant/components/squeezebox/* @rajlaud
homeassistant/components/starline/* @anonym-tsk homeassistant/components/starline/* @anonym-tsk
homeassistant/components/statistics/* @fabaff homeassistant/components/statistics/* @fabaff
homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/stiebel_eltron/* @fucm

View File

@ -1,3 +1,10 @@
"""Constants for the Squeezebox component.""" """Constants for the Squeezebox component."""
from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING
DOMAIN = "squeezebox" DOMAIN = "squeezebox"
SERVICE_CALL_METHOD = "call_method" SERVICE_CALL_METHOD = "call_method"
SQUEEZEBOX_MODE = {
"pause": STATE_PAUSED,
"play": STATE_PLAYING,
"stop": STATE_IDLE,
}

View File

@ -2,5 +2,6 @@
"domain": "squeezebox", "domain": "squeezebox",
"name": "Logitech Squeezebox", "name": "Logitech Squeezebox",
"documentation": "https://www.home-assistant.io/integrations/squeezebox", "documentation": "https://www.home-assistant.io/integrations/squeezebox",
"codeowners": [] "codeowners": ["@rajlaud"],
"requirements": ["pysqueezebox==0.1.2"]
} }

View File

@ -1,12 +1,9 @@
"""Support for interfacing to the Logitech SqueezeBox API.""" """Support for interfacing to the Logitech SqueezeBox API."""
import asyncio import asyncio
import json
import logging import logging
import socket import socket
import urllib.parse
import aiohttp from pysqueezebox import Server
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
@ -33,18 +30,14 @@ from homeassistant.const import (
CONF_PASSWORD, CONF_PASSWORD,
CONF_PORT, CONF_PORT,
CONF_USERNAME, CONF_USERNAME,
HTTP_OK,
STATE_IDLE,
STATE_OFF, STATE_OFF,
STATE_PAUSED,
STATE_PLAYING,
) )
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow 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__) _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) # Get IP of host, to prevent duplication of same host (different DNS names)
try: try:
ipaddr = socket.gethostbyname(host) ipaddr = await hass.async_add_executor_job(socket.gethostbyname, host)
except OSError as error: except OSError as error:
_LOGGER.error("Could not communicate with %s:%d: %s", host, port, error) _LOGGER.error("Could not communicate with %s:%d: %s", host, port, error)
raise PlatformNotReady from error raise PlatformNotReady from error
@ -135,16 +128,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
return return
_LOGGER.debug("Creating LMS object for %s", ipaddr) _LOGGER.debug("Creating LMS object for %s", ipaddr)
lms = LogitechMediaServer(hass, host, port, username, password) lms = Server(async_get_clientsession(hass), host, port, username, password)
players = await lms.create_players()
if players is None:
raise PlatformNotReady
known_servers.add(ipaddr) known_servers.add(ipaddr)
hass.data[DATA_SQUEEZEBOX].extend(players) players = await lms.async_get_players()
async_add_entities(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): async def async_service_handler(service):
"""Map services to methods on MediaPlayerEntity.""" """Map services to methods on MediaPlayerEntity."""
@ -182,133 +177,41 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
return True 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): 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.""" """Initialize the SqueezeBox device."""
super().__init__() self._player = player
self._lms = lms
self._id = player_id
self._status = {}
self._name = name
self._last_update = None self._last_update = None
_LOGGER.debug("Creating SqueezeBox object: %s, %s", name, player_id)
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
return self._name return self._player.name
@property @property
def unique_id(self): def unique_id(self):
"""Return a unique ID.""" """Return a unique ID."""
return self._id return self._player.player_id
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """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 return STATE_OFF
if "mode" in self._status: if self._player.mode:
if self._status["mode"] == "pause": return SQUEEZEBOX_MODE.get(self._player.mode)
return STATE_PAUSED
if self._status["mode"] == "play":
return STATE_PLAYING
if self._status["mode"] == "stop":
return STATE_IDLE
return None 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): async def async_update(self):
"""Retrieve the current state of the player.""" """Update the Player() object."""
tags = "adKl"
response = await self.async_query("status", "-", "1", f"tags:{tags}")
if response is False:
return
last_media_position = self.media_position last_media_position = self.media_position
await self._player.async_update()
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)
if self.media_position != last_media_position: if self.media_position != last_media_position:
_LOGGER.debug( _LOGGER.debug(
"Media position updated for %s: %s", self, self.media_position "Media position updated for %s: %s", self, self.media_position
@ -318,20 +221,18 @@ class SqueezeBoxDevice(MediaPlayerEntity):
@property @property
def volume_level(self): def volume_level(self):
"""Volume level of the media player (0..1).""" """Volume level of the media player (0..1)."""
if "mixer volume" in self._status: if self._player.volume:
return int(float(self._status["mixer volume"])) / 100.0 return int(float(self._player.volume)) / 100.0
@property @property
def is_volume_muted(self): def is_volume_muted(self):
"""Return true if volume is muted.""" """Return true if volume is muted."""
if "mixer volume" in self._status: return self._player.muting
return str(self._status["mixer volume"]).startswith("-")
@property @property
def media_content_id(self): def media_content_id(self):
"""Content ID of current playing media.""" """Content ID of current playing media."""
if "current_title" in self._status: return self._player.url
return self._status["current_title"]
@property @property
def media_content_type(self): def media_content_type(self):
@ -341,14 +242,12 @@ class SqueezeBoxDevice(MediaPlayerEntity):
@property @property
def media_duration(self): def media_duration(self):
"""Duration of current playing media in seconds.""" """Duration of current playing media in seconds."""
if "duration" in self._status: return self._player.duration
return int(float(self._status["duration"]))
@property @property
def media_position(self): def media_position(self):
"""Duration of current playing media in seconds.""" """Position of current playing media in seconds."""
if "time" in self._status: return self._player.time
return int(float(self._status["time"]))
@property @property
def media_position_updated_at(self): def media_position_updated_at(self):
@ -358,60 +257,27 @@ class SqueezeBoxDevice(MediaPlayerEntity):
@property @property
def media_image_url(self): def media_image_url(self):
"""Image url of current playing media.""" """Image url of current playing media."""
if "artwork_url" in self._status: return self._player.image_url
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
@property @property
def media_title(self): def media_title(self):
"""Title of current playing media.""" """Title of current playing media."""
if "title" in self._status: return self._player.title
return self._status["title"]
if "current_title" in self._status:
return self._status["current_title"]
@property @property
def media_artist(self): def media_artist(self):
"""Artist of current playing media.""" """Artist of current playing media."""
if "artist" in self._status: return self._player.artist
return self._status["artist"]
@property @property
def media_album_name(self): def media_album_name(self):
"""Album of current playing media.""" """Album of current playing media."""
if "album" in self._status: return self._player.album
return self._status["album"]
@property @property
def shuffle(self): def shuffle(self):
"""Boolean if shuffle is enabled.""" """Boolean if shuffle is enabled."""
if "playlist_shuffle" in self._status: return self._player.shuffle
return self._status["playlist_shuffle"] == 1
@property @property
def supported_features(self): def supported_features(self):
@ -420,53 +286,52 @@ class SqueezeBoxDevice(MediaPlayerEntity):
async def async_turn_off(self): async def async_turn_off(self):
"""Turn off media player.""" """Turn off media player."""
await self.async_query("power", "0") await self._player.async_set_power(False)
async def async_volume_up(self): async def async_volume_up(self):
"""Volume up media player.""" """Volume up media player."""
await self.async_query("mixer", "volume", "+5") await self._player.async_set_volume("+5")
async def async_volume_down(self): async def async_volume_down(self):
"""Volume down media player.""" """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): async def async_set_volume_level(self, volume):
"""Set volume level, range 0..1.""" """Set volume level, range 0..1."""
volume_percent = str(int(volume * 100)) 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): async def async_mute_volume(self, mute):
"""Mute (true) or unmute (false) media player.""" """Mute (true) or unmute (false) media player."""
mute_numeric = "1" if mute else "0" await self._player.async_set_muting(mute)
await self.async_query("mixer", "muting", mute_numeric)
async def async_media_play_pause(self): async def async_media_play_pause(self):
"""Send pause command to media player.""" """Send pause command to media player."""
await self.async_query("pause") await self._player.async_toggle_pause()
async def async_media_play(self): async def async_media_play(self):
"""Send play command to media player.""" """Send play command to media player."""
await self.async_query("play") await self._player.async_play()
async def async_media_pause(self): async def async_media_pause(self):
"""Send pause command to media player.""" """Send pause command to media player."""
await self.async_query("pause", "1") await self._player.async_pause()
async def async_media_next_track(self): async def async_media_next_track(self):
"""Send next track command.""" """Send next track command."""
await self.async_query("playlist", "index", "+1") await self._player.async_index("+1")
async def async_media_previous_track(self): async def async_media_previous_track(self):
"""Send next track command.""" """Send next track command."""
await self.async_query("playlist", "index", "-1") await self._player.async_index("-1")
async def async_media_seek(self, position): async def async_media_seek(self, position):
"""Send seek command.""" """Send seek command."""
await self.async_query("time", position) await self._player.async_time(position)
async def async_turn_on(self): async def async_turn_on(self):
"""Turn the media player on.""" """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): 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. If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the current playlist.
""" """
cmd = "play"
if kwargs.get(ATTR_MEDIA_ENQUEUE): if kwargs.get(ATTR_MEDIA_ENQUEUE):
await self._add_uri_to_playlist(media_id) cmd = "add"
return
await self._play_uri(media_id) await self._player.async_load_url(media_id, cmd)
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)
async def async_set_shuffle(self, shuffle): async def async_set_shuffle(self, shuffle):
"""Enable/disable shuffle mode.""" """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): async def async_clear_playlist(self):
"""Send the media player the command for clear playlist.""" """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): async def async_call_method(self, command, parameters=None):
""" """
@ -507,4 +365,4 @@ class SqueezeBoxDevice(MediaPlayerEntity):
if parameters: if parameters:
for parameter in parameters: for parameter in parameters:
all_params.append(parameter) all_params.append(parameter)
await self.async_query(*all_params) await self._player.async_query(*all_params)

View File

@ -1580,6 +1580,9 @@ pysonos==0.0.25
# homeassistant.components.spc # homeassistant.components.spc
pyspcwebgw==0.4.0 pyspcwebgw==0.4.0
# homeassistant.components.squeezebox
pysqueezebox==0.1.2
# homeassistant.components.stiebel_eltron # homeassistant.components.stiebel_eltron
pystiebeleltron==0.0.1.dev2 pystiebeleltron==0.0.1.dev2