From ba394fd2aa69f82527574ecfdf7e1164ac92740a Mon Sep 17 00:00:00 2001 From: BarrettLowe Date: Tue, 14 Apr 2020 13:22:01 -0500 Subject: [PATCH] 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 * 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 --- .coveragerc | 2 +- homeassistant/components/snapcast/__init__.py | 49 -------- homeassistant/components/snapcast/const.py | 17 +++ .../components/snapcast/media_player.py | 112 +++++++++++------- .../components/snapcast/services.yaml | 9 ++ 5 files changed, 95 insertions(+), 94 deletions(-) create mode 100644 homeassistant/components/snapcast/const.py diff --git a/.coveragerc b/.coveragerc index acf72eb254c..f1f4aeaf0d1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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 diff --git a/homeassistant/components/snapcast/__init__.py b/homeassistant/components/snapcast/__init__.py index e6c574b7b2b..b5279fa3ce0 100644 --- a/homeassistant/components/snapcast/__init__.py +++ b/homeassistant/components/snapcast/__init__.py @@ -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 diff --git a/homeassistant/components/snapcast/const.py b/homeassistant/components/snapcast/const.py new file mode 100644 index 00000000000..674a22993b9 --- /dev/null +++ b/homeassistant/components/snapcast/const.py @@ -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" diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 004dd36ed4a..1bd44959ab1 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -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() diff --git a/homeassistant/components/snapcast/services.yaml b/homeassistant/components/snapcast/services.yaml index 1517f12f52d..3b83aa3774d 100644 --- a/homeassistant/components/snapcast/services.yaml +++ b/homeassistant/components/snapcast/services.yaml @@ -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