Add snapcast latency attribute and service (#34126)

* Implemented snapcast latency attributes

* Code review changes and Snapcast maintenance

Updated how entity services get called - now conforms to most
current method

* Cleanup tasks
Moved constants into separate file
Removed unnecessary logger message
Remove unnecessary schemas

* FIx linting errors

* Sort imports

* Update with requested change

Better - use next()

Co-Authored-By: Martin Hjelmare <marhje52@gmail.com>

* Add guards for bad service calls

* Add back in platform schema

* Add check for unjoin service call

* Fix lint/format

* remove comma inserted by black

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
BarrettLowe 2020-04-14 13:22:01 -05:00 committed by GitHub
parent 535ce8726d
commit ba394fd2aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 95 additions and 94 deletions

View File

@ -648,7 +648,7 @@ omit =
homeassistant/components/smarthab/*
homeassistant/components/sms/*
homeassistant/components/smtp/notify.py
homeassistant/components/snapcast/media_player.py
homeassistant/components/snapcast/*
homeassistant/components/snmp/*
homeassistant/components/sochain/sensor.py
homeassistant/components/socialblade/sensor.py

View File

@ -1,50 +1 @@
"""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

View File

@ -0,0 +1,17 @@
"""Constants for Snapcast."""
DATA_KEY = "snapcast"
GROUP_PREFIX = "snapcast_group_"
GROUP_SUFFIX = "Snapcast Group"
CLIENT_PREFIX = "snapcast_client_"
CLIENT_SUFFIX = "Snapcast Client"
SERVICE_SNAPSHOT = "snapshot"
SERVICE_RESTORE = "restore"
SERVICE_JOIN = "join"
SERVICE_UNJOIN = "unjoin"
SERVICE_SET_LATENCY = "set_latency"
ATTR_MASTER = "master"
ATTR_LATENCY = "latency"

View File

@ -13,7 +13,6 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_SET,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_HOST,
CONF_PORT,
STATE_IDLE,
@ -22,22 +21,25 @@ from homeassistant.const import (
STATE_PLAYING,
STATE_UNKNOWN,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers import config_validation as cv, entity_platform
from . import (
from .const import (
ATTR_LATENCY,
ATTR_MASTER,
DOMAIN,
CLIENT_PREFIX,
CLIENT_SUFFIX,
DATA_KEY,
GROUP_PREFIX,
GROUP_SUFFIX,
SERVICE_JOIN,
SERVICE_RESTORE,
SERVICE_SET_LATENCY,
SERVICE_SNAPSHOT,
SERVICE_UNJOIN,
)
_LOGGER = logging.getLogger(__name__)
DATA_KEY = "snapcast"
SUPPORT_SNAPCAST_CLIENT = (
SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | SUPPORT_SELECT_SOURCE
)
@ -45,11 +47,6 @@ 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"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT): cv.port}
)
@ -61,33 +58,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
host = config.get(CONF_HOST)
port = config.get(CONF_PORT, CONTROL_PORT)
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_SNAPSHOT:
device.snapshot()
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()
async_dispatcher_connect(hass, DOMAIN, async_service_handle)
platform = entity_platform.current_platform.get()
platform.async_register_entity_service(SERVICE_SNAPSHOT, {}, "snapshot")
platform.async_register_entity_service(SERVICE_RESTORE, {}, "async_restore")
platform.async_register_entity_service(
SERVICE_JOIN, {vol.Required(ATTR_MASTER): cv.entity_id}, handle_async_join
)
platform.async_register_entity_service(SERVICE_UNJOIN, {}, handle_async_unjoin)
platform.async_register_entity_service(
SERVICE_SET_LATENCY,
{vol.Required(ATTR_LATENCY): cv.positive_int},
handle_set_latency,
)
try:
server = await snapcast.control.create_server(
@ -107,6 +89,27 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(devices)
async def handle_async_join(entity, service_call):
"""Handle the entity service join."""
if not isinstance(entity, SnapcastClientDevice):
raise ValueError("Entity is not a client. Can only join clients.")
await entity.async_join(service_call.data[ATTR_MASTER])
async def handle_async_unjoin(entity, service_call):
"""Handle the entity service unjoin."""
if not isinstance(entity, SnapcastClientDevice):
raise ValueError("Entity is not a client. Can only unjoin clients.")
await entity.async_unjoin()
async def handle_set_latency(entity, service_call):
"""Handle the entity service set_latency."""
if not isinstance(entity, SnapcastClientDevice):
raise ValueError("Latency can only be set for a Snapcast client.")
await entity.async_set_latency(service_call.data[ATTR_LATENCY])
class SnapcastGroupDevice(MediaPlayerDevice):
"""Representation of a Snapcast group device."""
@ -260,14 +263,23 @@ class SnapcastClientDevice(MediaPlayerDevice):
@property
def device_state_attributes(self):
"""Return the state attributes."""
state_attrs = {}
if self.latency is not None:
state_attrs["latency"] = self.latency
name = f"{self._client.friendly_name} {CLIENT_SUFFIX}"
return {"friendly_name": name}
state_attrs["friendly_name"] = name
return state_attrs
@property
def should_poll(self):
"""Do not poll for state."""
return False
@property
def latency(self):
"""Latency for Client."""
return self._client.latency
async def async_select_source(self, source):
"""Set input source."""
streams = self._client.group.streams_by_name()
@ -287,12 +299,19 @@ class SnapcastClientDevice(MediaPlayerDevice):
async def async_join(self, master):
"""Join the group of the master player."""
master_group = [
master_entity = next(
entity for entity in self.hass.data[DATA_KEY] if entity.entity_id == master
)
if not isinstance(master_entity, SnapcastClientDevice):
raise ValueError("Master is not a client device. Can only join clients.")
master_group = next(
group
for group in self._client.groups_available()
if master.identifier in group.clients
]
await master_group[0].add_client(self._client.identifier)
if master_entity.identifier in group.clients
)
await master_group.add_client(self._client.identifier)
self.async_write_ha_state()
async def async_unjoin(self):
@ -307,3 +326,8 @@ class SnapcastClientDevice(MediaPlayerDevice):
async def async_restore(self):
"""Restore the client state."""
await self._client.restore()
async def async_set_latency(self, latency):
"""Set the latency of the client."""
await self._client.set_latency(latency)
self.async_write_ha_state()

View File

@ -28,3 +28,12 @@ restore:
entity_id:
description: Name(s) of entities that will be restored. Platform dependent.
example: "media_player.living_room"
set_latency:
description: Set client set_latency
fields:
entity_id:
description: Name of entities that will have adjusted latency
latency:
description: Latency in master
example: 14