update snapcast media player (#7079)

* update snapcast

* fix docstrings

* bump dep version

* address snapcast review comments

* add snapcast group volume support

* fix snapcast requirements

* update snapcast client entity id

* snapshot/restore functions

* refactor snapshot/restore services

* clean up

* update snapcast req

* bump version

* fix async updates
This commit is contained in:
happyleavesaoc 2017-05-30 05:34:39 -04:00 committed by Pascal Vizeli
parent 91806bfa2a
commit 96b20b3a97
3 changed files with 198 additions and 41 deletions

View File

@ -165,6 +165,22 @@ shuffle_set:
description: True/false for enabling/disabling shuffle description: True/false for enabling/disabling shuffle
example: true example: true
snapcast_snapshot:
description: Take a snapshot of the media player.
fields:
entity_id:
description: Name(s) of entites that will be snapshotted. Platform dependent.
example: 'media_player.living_room'
snapcast_restore:
description: Restore a snapshot of the media player.
fields:
entity_id:
description: Name(s) of entites that will be restored. Platform dependent.
example: 'media_player.living_room'
sonos_join: sonos_join:
description: Group player together. description: Group player together.

View File

@ -4,62 +4,197 @@ Support for interacting with Snapcast clients.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.snapcast/ https://home-assistant.io/components/media_player.snapcast/
""" """
import asyncio
import logging import logging
from os import path
import socket import socket
import voluptuous as vol import voluptuous as vol
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE,
SUPPORT_PLAY, PLATFORM_SCHEMA, MediaPlayerDevice) PLATFORM_SCHEMA, MediaPlayerDevice)
from homeassistant.const import ( from homeassistant.const import (
STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, CONF_HOST, CONF_PORT) STATE_ON, STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, CONF_HOST,
CONF_PORT, ATTR_ENTITY_ID)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['snapcast==1.2.2'] REQUIREMENTS = ['snapcast==2.0.6']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'snapcast' DOMAIN = 'snapcast'
SUPPORT_SNAPCAST = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SERVICE_SNAPSHOT = 'snapcast_snapshot'
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY SERVICE_RESTORE = 'snapcast_restore'
SUPPORT_SNAPCAST_CLIENT = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET
SUPPORT_SNAPCAST_GROUP = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET |\
SUPPORT_SELECT_SOURCE
GROUP_PREFIX = 'snapcast_group_'
GROUP_SUFFIX = 'Snapcast Group'
CLIENT_PREFIX = 'snapcast_client_'
CLIENT_SUFFIX = 'Snapcast Client'
SERVICE_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_PORT): cv.port
}) })
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the Snapcast platform.""" """Setup the Snapcast platform."""
import snapcast.control import snapcast.control
from snapcast.control.server import CONTROL_PORT
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
port = config.get(CONF_PORT, snapcast.control.CONTROL_PORT) port = config.get(CONF_PORT, CONTROL_PORT)
@asyncio.coroutine
def _handle_service(service):
"""Handle services."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
devices = [device for device in hass.data[DOMAIN]
if device.entity_id in entity_ids]
for device in devices:
if service.service == SERVICE_SNAPSHOT:
device.snapshot()
elif service.service == SERVICE_RESTORE:
yield from device.async_restore()
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.async_register(
DOMAIN, SERVICE_SNAPSHOT, _handle_service,
descriptions.get(SERVICE_SNAPSHOT), schema=SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_RESTORE, _handle_service,
descriptions.get(SERVICE_RESTORE), schema=SERVICE_SCHEMA)
try: try:
server = snapcast.control.Snapserver(host, port) server = yield from snapcast.control.create_server(
hass.loop, host, port)
except socket.gaierror: except socket.gaierror:
_LOGGER.error( _LOGGER.error('Could not connect to Snapcast server at %s:%d',
"Could not connect to Snapcast server at %s:%d", host, port) host, port)
return False
groups = [SnapcastGroupDevice(group) for group in server.groups]
clients = [SnapcastClientDevice(client) for client in server.clients]
devices = groups + clients
hass.data[DOMAIN] = devices
async_add_devices(devices)
return True
class SnapcastGroupDevice(MediaPlayerDevice):
"""Representation of a Snapcast group device."""
def __init__(self, group):
"""Initialize the Snapcast group device."""
group.set_callback(self.schedule_update_ha_state)
self._group = group
@property
def state(self):
"""Return the state of the player."""
return {
'idle': STATE_IDLE,
'playing': STATE_PLAYING,
'unknown': STATE_UNKNOWN,
}.get(self._group.stream_status, STATE_UNKNOWN)
@property
def name(self):
"""Return the name of the device."""
return '{}{}'.format(GROUP_PREFIX, self._group.identifier)
@property
def source(self):
"""Return the current input source."""
return self._group.stream
@property
def volume_level(self):
"""Return the volume level."""
return self._group.volume / 100
@property
def is_volume_muted(self):
"""Volume muted."""
return self._group.muted
@property
def supported_features(self):
"""Flag media player features that are supported."""
return SUPPORT_SNAPCAST_GROUP
@property
def source_list(self):
"""List of available input sources."""
return list(self._group.streams_by_name().keys())
@property
def device_state_attributes(self):
"""Return the state attributes."""
name = '{} {}'.format(self._group.friendly_name, GROUP_SUFFIX)
return {
'friendly_name': name
}
@property
def should_poll(self):
"""Do not poll for state."""
return False return False
add_devices([SnapcastDevice(client) for client in server.clients]) @asyncio.coroutine
def async_select_source(self, source):
"""Set input source."""
streams = self._group.streams_by_name()
if source in streams:
yield from self._group.set_stream(streams[source].identifier)
self.hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
def async_mute_volume(self, mute):
"""Send the mute command."""
yield from self._group.set_muted(mute)
self.hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
def async_set_volume_level(self, volume):
"""Set the volume level."""
yield from self._group.set_volume(round(volume * 100))
self.hass.async_add_job(self.async_update_ha_state())
def snapshot(self):
"""Snapshot the group state."""
self._group.snapshot()
@asyncio.coroutine
def async_restore(self):
"""Restore the group state."""
yield from self._group.restore()
class SnapcastDevice(MediaPlayerDevice): class SnapcastClientDevice(MediaPlayerDevice):
"""Representation of a Snapcast client device.""" """Representation of a Snapcast client device."""
def __init__(self, client): def __init__(self, client):
"""Initialize the Snapcast device.""" """Initialize the Snapcast client device."""
client.set_callback(self.schedule_update_ha_state)
self._client = client self._client = client
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
return self._client.identifier return '{}{}'.format(CLIENT_PREFIX, self._client.identifier)
@property @property
def volume_level(self): def volume_level(self):
@ -74,39 +209,45 @@ class SnapcastDevice(MediaPlayerDevice):
@property @property
def supported_features(self): def supported_features(self):
"""Flag media player features that are supported.""" """Flag media player features that are supported."""
return SUPPORT_SNAPCAST return SUPPORT_SNAPCAST_CLIENT
@property @property
def state(self): def state(self):
"""Return the state of the player.""" """Return the state of the player."""
if not self._client.connected: if self._client.connected:
return STATE_ON
return STATE_OFF return STATE_OFF
@property
def device_state_attributes(self):
"""Return the state attributes."""
name = '{} {}'.format(self._client.friendly_name, CLIENT_SUFFIX)
return { return {
'idle': STATE_IDLE, 'friendly_name': name
'playing': STATE_PLAYING, }
'unknown': STATE_UNKNOWN,
}.get(self._client.stream.status, STATE_UNKNOWN)
@property @property
def source(self): def should_poll(self):
"""Return the current input source.""" """Do not poll for state."""
return self._client.stream.name return False
@property @asyncio.coroutine
def source_list(self): def async_mute_volume(self, mute):
"""List of available input sources."""
return list(self._client.streams_by_name().keys())
def mute_volume(self, mute):
"""Send the mute command.""" """Send the mute command."""
self._client.muted = mute yield from self._client.set_muted(mute)
self.hass.async_add_job(self.async_update_ha_state())
def set_volume_level(self, volume): @asyncio.coroutine
def async_set_volume_level(self, volume):
"""Set the volume level.""" """Set the volume level."""
self._client.volume = round(volume * 100) yield from self._client.set_volume(round(volume * 100))
self.hass.async_add_job(self.async_update_ha_state())
def select_source(self, source): def snapshot(self):
"""Set input source.""" """Snapshot the client state."""
streams = self._client.streams_by_name() self._client.snapshot()
if source in streams:
self._client.stream = streams[source].identifier @asyncio.coroutine
def async_restore(self):
"""Restore the client state."""
yield from self._client.restore()

View File

@ -784,7 +784,7 @@ sleepyq==0.6
# smbus-cffi==0.5.1 # smbus-cffi==0.5.1
# homeassistant.components.media_player.snapcast # homeassistant.components.media_player.snapcast
snapcast==1.2.2 snapcast==2.0.6
# homeassistant.components.climate.honeywell # homeassistant.components.climate.honeywell
somecomfort==0.4.1 somecomfort==0.4.1