Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiroom support for snapcast #24061

Merged
merged 5 commits into from
Jul 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions homeassistant/components/media_player/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -171,20 +171,6 @@ shuffle_set:
description: True/false for enabling/disabling shuffle.
example: true

snapcast_snapshot:
description: Take a snapshot of the media player.
fields:
entity_id:
description: Name(s) of entities 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 entities that will be restored. Platform dependent.
example: 'media_player.living_room'

channels_seek_forward:
description: Seek forward by a set number of seconds.
fields:
Expand Down
47 changes: 47 additions & 0 deletions homeassistant/components/snapcast/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,48 @@
"""The snapcast component."""

import asyncio
import voluptuous as vol

from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send

DOMAIN = 'snapcast'

SERVICE_SNAPSHOT = 'snapshot'
SERVICE_RESTORE = 'restore'
SERVICE_JOIN = 'join'
SERVICE_UNJOIN = 'unjoin'

ATTR_MASTER = 'master'

SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
})

JOIN_SERVICE_SCHEMA = SERVICE_SCHEMA.extend({
vol.Required(ATTR_MASTER): cv.entity_id,
})


async def async_setup(hass, config):
"""Handle service configuration."""
service_event = asyncio.Event()

async def service_handle(service):
"""Dispatch a service call."""
service_event.clear()
async_dispatcher_send(hass, DOMAIN, service_event, service.service,
service.data)
await service_event.wait()

hass.services.async_register(
DOMAIN, SERVICE_SNAPSHOT, service_handle, schema=SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_RESTORE, service_handle, schema=SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_JOIN, service_handle, schema=JOIN_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_UNJOIN, service_handle, schema=SERVICE_SCHEMA)

return True
2 changes: 1 addition & 1 deletion homeassistant/components/snapcast/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "Snapcast",
"documentation": "https://www.home-assistant.io/components/snapcast",
"requirements": [
"snapcast==2.0.9"
"snapcast==2.0.10"
],
"dependencies": [],
"codeowners": []
Expand Down
77 changes: 59 additions & 18 deletions homeassistant/components/snapcast/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,24 @@
from homeassistant.components.media_player import (
MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.components.media_player.const import (
DOMAIN, SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE,
SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET)
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, STATE_IDLE, STATE_OFF, STATE_ON,
STATE_PLAYING, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect

from . import (
DOMAIN, SERVICE_SNAPSHOT, SERVICE_RESTORE, SERVICE_JOIN,
SERVICE_UNJOIN, ATTR_MASTER)

_LOGGER = logging.getLogger(__name__)

DATA_KEY = 'snapcast'

SERVICE_SNAPSHOT = 'snapcast_snapshot'
SERVICE_RESTORE = 'snapcast_restore'

SUPPORT_SNAPCAST_CLIENT = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET
SUPPORT_SNAPCAST_CLIENT = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET |\
SUPPORT_SELECT_SOURCE
SUPPORT_SNAPCAST_GROUP = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET |\
SUPPORT_SELECT_SOURCE

Expand All @@ -30,10 +33,6 @@
CLIENT_PREFIX = 'snapcast_client_'
CLIENT_SUFFIX = 'Snapcast Client'

SERVICE_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
})

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT): cv.port,
Expand All @@ -48,21 +47,29 @@ async def async_setup_platform(hass, config, async_add_entities,
host = config.get(CONF_HOST)
port = config.get(CONF_PORT, CONTROL_PORT)

async def _handle_service(service):
"""Handle services."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
async def async_service_handle(service_event, service, data):
"""Handle dispatched services."""
entity_ids = data.get(ATTR_ENTITY_ID)
devices = [device for device in hass.data[DATA_KEY]
if device.entity_id in entity_ids]
for device in devices:
if service.service == SERVICE_SNAPSHOT:
if service == SERVICE_SNAPSHOT:
device.snapshot()
elif service.service == SERVICE_RESTORE:
elif service == SERVICE_RESTORE:
await device.async_restore()
elif service == SERVICE_JOIN:
if isinstance(device, SnapcastClientDevice):
master = [e for e in hass.data[DATA_KEY]
if e.entity_id == data[ATTR_MASTER]]
if isinstance(master[0], SnapcastClientDevice):
await device.async_join(master[0])
elif service == SERVICE_UNJOIN:
if isinstance(device, SnapcastClientDevice):
await device.async_unjoin()

service_event.set()

hass.services.async_register(
DOMAIN, SERVICE_SNAPSHOT, _handle_service, schema=SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_RESTORE, _handle_service, schema=SERVICE_SCHEMA)
async_dispatcher_connect(hass, DOMAIN, async_service_handle)

try:
server = await snapcast.control.create_server(
Expand Down Expand Up @@ -194,11 +201,21 @@ def unique_id(self):
"""
return self._uid

@property
def identifier(self):
"""Return the snapcast identifier."""
return self._client.identifier

@property
def name(self):
"""Return the name of the device."""
return '{}{}'.format(CLIENT_PREFIX, self._client.identifier)

@property
def source(self):
"""Return the current input source."""
return self._client.group.stream

@property
def volume_level(self):
"""Return the volume level."""
Expand All @@ -214,6 +231,11 @@ def supported_features(self):
"""Flag media player features that are supported."""
return SUPPORT_SNAPCAST_CLIENT

@property
def source_list(self):
"""List of available input sources."""
return list(self._client.group.streams_by_name().keys())

@property
def state(self):
"""Return the state of the player."""
Expand All @@ -234,6 +256,13 @@ def should_poll(self):
"""Do not poll for state."""
return False

async def async_select_source(self, source):
"""Set input source."""
streams = self._client.group.streams_by_name()
if source in streams:
await self._client.group.set_stream(streams[source].identifier)
self.async_schedule_update_ha_state()

async def async_mute_volume(self, mute):
"""Send the mute command."""
await self._client.set_muted(mute)
Expand All @@ -244,6 +273,18 @@ async def async_set_volume_level(self, volume):
await self._client.set_volume(round(volume * 100))
self.async_schedule_update_ha_state()

async def async_join(self, master):
"""Join the group of the master player."""
master_group = [group for group in self._client.groups_available()
if master.identifier in group.clients]
await master_group[0].add_client(self._client.identifier)
self.async_schedule_update_ha_state()

async def async_unjoin(self):
"""Unjoin the group the player is currently in."""
await self._client.group.remove_client(self._client.identifier)
self.async_schedule_update_ha_state()

def snapshot(self):
"""Snapshot the client state."""
self._client.snapshot()
Expand Down
30 changes: 30 additions & 0 deletions homeassistant/components/snapcast/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
join:
description: Group players together.
fields:
master:
description: Entity ID of the player to synchronize to.
example: 'media_player.living_room'
entity_id:
description: Entity ID of the players to join to the "master".
example: 'media_player.bedroom'

unjoin:
description: Unjoin the player from a group.
fields:
entity_id:
description: Entity ID of the player to unjoin.
example: 'media_player.living_room'

snapshot:
description: Take a snapshot of the media player.
fields:
entity_id:
description: Name(s) of entities that will be snapshotted. Platform dependent.
example: 'media_player.living_room'

restore:
description: Restore a snapshot of the media player.
fields:
entity_id:
description: Name(s) of entities that will be restored. Platform dependent.
example: 'media_player.living_room'
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1638,7 +1638,7 @@ smarthab==0.20
smhi-pkg==1.0.10

# homeassistant.components.snapcast
snapcast==2.0.9
snapcast==2.0.10

# homeassistant.components.socialblade
socialbladeclient==0.2
Expand Down