diff --git a/.coveragerc b/.coveragerc index c550bc407a6..8cc8077312a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -465,6 +465,7 @@ omit = homeassistant/components/media_player/russound_rio.py homeassistant/components/media_player/russound_rnet.py homeassistant/components/media_player/snapcast.py + homeassistant/components/media_player/songpal.py homeassistant/components/media_player/sonos.py homeassistant/components/media_player/spotify.py homeassistant/components/media_player/squeezebox.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 980ac7d661c..d1045143bb2 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -71,6 +71,7 @@ SERVICE_HANDLERS = { 'sabnzbd': ('sensor', 'sabnzbd'), 'bose_soundtouch': ('media_player', 'soundtouch'), 'bluesound': ('media_player', 'bluesound'), + 'songpal': ('media_player', 'songpal'), } CONF_IGNORE = 'ignore' diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 7ac250b1d30..4d488a92300 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -364,3 +364,17 @@ bluesound_clear_sleep_timer: entity_id: description: Name(s) of entities that will have the timer cleared. example: 'media_player.bluesound_livingroom' + +songpal_set_sound_setting: + description: Change sound setting. + + fields: + entity_id: + description: Target device. + example: 'media_player.my_soundbar' + name: + description: Name of the setting. + example: 'nightMode' + value: + description: Value to set. + example: 'on' diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py new file mode 100644 index 00000000000..83637633ba1 --- /dev/null +++ b/homeassistant/components/media_player/songpal.py @@ -0,0 +1,252 @@ +""" +Support for Songpal-enabled (Sony) media devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.songpal/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.media_player import ( + PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_SET, + SUPPORT_TURN_ON, MediaPlayerDevice, DOMAIN) +from homeassistant.const import ( + CONF_NAME, STATE_ON, STATE_OFF, ATTR_ENTITY_ID) +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['python-songpal==0.0.6'] + +SUPPORT_SONGPAL = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | \ + SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF + +_LOGGER = logging.getLogger(__name__) + + +PLATFORM = "songpal" + +SET_SOUND_SETTING = "songpal_set_sound_setting" + +PARAM_NAME = "name" +PARAM_VALUE = "value" + +CONF_ENDPOINT = "endpoint" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_ENDPOINT): cv.string, +}) + +SET_SOUND_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(PARAM_NAME): cv.string, + vol.Required(PARAM_VALUE): cv.string}) + + +async def async_setup_platform(hass, config, + async_add_devices, discovery_info=None): + """Set up the Songpal platform.""" + from songpal import SongpalException + if PLATFORM not in hass.data: + hass.data[PLATFORM] = {} + + if discovery_info is not None: + name = discovery_info["name"] + endpoint = discovery_info["properties"]["endpoint"] + _LOGGER.debug("Got autodiscovered %s - endpoint: %s", name, endpoint) + + device = SongpalDevice(name, endpoint) + else: + name = config.get(CONF_NAME) + endpoint = config.get(CONF_ENDPOINT) + device = SongpalDevice(name, endpoint) + + try: + await device.initialize() + except SongpalException as ex: + _LOGGER.error("Unable to get methods from songpal: %s", ex) + raise PlatformNotReady + + hass.data[PLATFORM][endpoint] = device + + async_add_devices([device], True) + + async def async_service_handler(service): + """Service handler.""" + entity_id = service.data.get("entity_id", None) + params = {key: value for key, value in service.data.items() + if key != ATTR_ENTITY_ID} + + for device in hass.data[PLATFORM].values(): + if device.entity_id == entity_id or entity_id is None: + _LOGGER.debug("Calling %s (entity: %s) with params %s", + service, entity_id, params) + + await device.async_set_sound_setting(params[PARAM_NAME], + params[PARAM_VALUE]) + + hass.services.async_register( + DOMAIN, SET_SOUND_SETTING, async_service_handler, + schema=SET_SOUND_SCHEMA) + + +class SongpalDevice(MediaPlayerDevice): + """Class representing a Songpal device.""" + + def __init__(self, name, endpoint): + """Init.""" + import songpal + self._name = name + self.endpoint = endpoint + self.dev = songpal.Protocol(self.endpoint) + self._sysinfo = None + + self._state = False + self._available = False + self._initialized = False + + self._volume_control = None + self._volume_min = 0 + self._volume_max = 1 + self._volume = 0 + self._is_muted = False + + self._sources = [] + + async def initialize(self): + """Initialize the device.""" + await self.dev.get_supported_methods() + self._sysinfo = await self.dev.get_system_info() + + @property + def name(self): + """Return name of the device.""" + return self._name + + @property + def unique_id(self): + """Return an unique ID.""" + return self._sysinfo.macAddr + + @property + def available(self): + """Return availability of the device.""" + return self._available + + async def async_set_sound_setting(self, name, value): + """Change a setting on the device.""" + await self.dev.set_sound_settings(name, value) + + async def async_update(self): + """Fetch updates from the device.""" + from songpal import SongpalException + try: + volumes = await self.dev.get_volume_information() + if not volumes: + _LOGGER.error("Got no volume controls, bailing out") + self._available = False + return + + if len(volumes) > 1: + _LOGGER.warning("Got %s volume controls, using the first one", + volumes) + + volume = volumes.pop() + _LOGGER.debug("Current volume: %s", volume) + + self._volume_max = volume.maxVolume + self._volume_min = volume.minVolume + self._volume = volume.volume + self._volume_control = volume + self._is_muted = self._volume_control.is_muted + + status = await self.dev.get_power() + self._state = status.status + _LOGGER.debug("Got state: %s", status) + + inputs = await self.dev.get_inputs() + _LOGGER.debug("Got ins: %s", inputs) + self._sources = inputs + + self._available = True + except SongpalException as ex: + # if we were available, print out the exception + if self._available: + _LOGGER.error("Got an exception: %s", ex) + self._available = False + + async def async_select_source(self, source): + """Select source.""" + for out in self._sources: + if out.title == source: + await out.activate() + return + + _LOGGER.error("Unable to find output: %s", source) + + @property + def source_list(self): + """Return list of available sources.""" + return [x.title for x in self._sources] + + @property + def state(self): + """Return current state.""" + if self._state: + return STATE_ON + return STATE_OFF + + @property + def source(self): + """Return currently active source.""" + for out in self._sources: + if out.active: + return out.title + + return None + + @property + def volume_level(self): + """Return volume level.""" + volume = self._volume / self._volume_max + return volume + + async def async_set_volume_level(self, volume): + """Set volume level.""" + volume = int(volume * self._volume_max) + _LOGGER.debug("Setting volume to %s", volume) + return await self._volume_control.set_volume(volume) + + async def async_volume_up(self): + """Set volume up.""" + return await self._volume_control.set_volume("+1") + + async def async_volume_down(self): + """Set volume down.""" + return await self._volume_control.set_volume("-1") + + async def async_turn_on(self): + """Turn the device on.""" + return await self.dev.set_power(True) + + async def async_turn_off(self): + """Turn the device off.""" + return await self.dev.set_power(False) + + async def async_mute_volume(self, mute): + """Mute or unmute the device.""" + _LOGGER.debug("Set mute: %s", mute) + return await self._volume_control.set_mute(mute) + + @property + def is_volume_muted(self): + """Return whether the device is muted.""" + return self._is_muted + + @property + def supported_features(self): + """Return supported features.""" + return SUPPORT_SONGPAL diff --git a/requirements_all.txt b/requirements_all.txt index 9cb0f4b6fc2..2082da5a89c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -958,6 +958,9 @@ python-roku==3.1.5 # homeassistant.components.sensor.sochain python-sochain-api==0.0.2 +# homeassistant.components.media_player.songpal +python-songpal==0.0.6 + # homeassistant.components.sensor.synologydsm python-synology==0.1.0