diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 89686c312bd47..34092b4cea493 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -10,7 +10,6 @@ import collections import hashlib import logging -import os from random import SystemRandom from aiohttp import web @@ -19,7 +18,6 @@ import voluptuous as vol from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView -from homeassistant.config import load_yaml_config_file from homeassistant.const import ( STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, @@ -57,6 +55,7 @@ SERVICE_PLAY_MEDIA = 'play_media' SERVICE_SELECT_SOURCE = 'select_source' +SERVICE_SELECT_SOUND_MODE = 'select_sound_mode' SERVICE_CLEAR_PLAYLIST = 'clear_playlist' ATTR_MEDIA_VOLUME_LEVEL = 'volume_level' @@ -81,6 +80,8 @@ ATTR_APP_NAME = 'app_name' ATTR_INPUT_SOURCE = 'source' ATTR_INPUT_SOURCE_LIST = 'source_list' +ATTR_SOUND_MODE = 'sound_mode' +ATTR_SOUND_MODE_LIST = 'sound_mode_list' ATTR_MEDIA_ENQUEUE = 'enqueue' ATTR_MEDIA_SHUFFLE = 'shuffle' @@ -90,6 +91,7 @@ MEDIA_TYPE_EPISODE = 'episode' MEDIA_TYPE_CHANNEL = 'channel' MEDIA_TYPE_PLAYLIST = 'playlist' +MEDIA_TYPE_URL = 'url' SUPPORT_PAUSE = 1 SUPPORT_SEEK = 2 @@ -107,6 +109,7 @@ SUPPORT_CLEAR_PLAYLIST = 8192 SUPPORT_PLAY = 16384 SUPPORT_SHUFFLE_SET = 32768 +SUPPORT_SELECT_SOUND_MODE = 65536 # Service call validation schemas MEDIA_PLAYER_SCHEMA = vol.Schema({ @@ -130,6 +133,10 @@ vol.Required(ATTR_INPUT_SOURCE): cv.string, }) +MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ + vol.Required(ATTR_SOUND_MODE): cv.string, +}) + MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, @@ -165,6 +172,9 @@ SERVICE_SELECT_SOURCE: { 'method': 'async_select_source', 'schema': MEDIA_PLAYER_SELECT_SOURCE_SCHEMA}, + SERVICE_SELECT_SOUND_MODE: { + 'method': 'async_select_sound_mode', + 'schema': MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA}, SERVICE_PLAY_MEDIA: { 'method': 'async_play_media', 'schema': MEDIA_PLAYER_PLAY_MEDIA_SCHEMA}, @@ -195,6 +205,8 @@ ATTR_APP_NAME, ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, + ATTR_SOUND_MODE, + ATTR_SOUND_MODE_LIST, ATTR_MEDIA_SHUFFLE, ] @@ -344,6 +356,17 @@ def select_source(hass, source, entity_id=None): hass.services.call(DOMAIN, SERVICE_SELECT_SOURCE, data) +@bind_hass +def select_sound_mode(hass, sound_mode, entity_id=None): + """Send the media player the command to select sound mode.""" + data = {ATTR_SOUND_MODE: sound_mode} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SELECT_SOUND_MODE, data) + + @bind_hass def clear_playlist(hass, entity_id=None): """Send the media player the command for clear playlist.""" @@ -368,14 +391,10 @@ def async_setup(hass, config): component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) - hass.http.register_view(MediaPlayerImageView(component.entities)) + hass.http.register_view(MediaPlayerImageView(component)) yield from component.async_setup(config) - descriptions = yield from hass.async_add_job( - load_yaml_config_file, os.path.join( - os.path.dirname(__file__), 'services.yaml')) - @asyncio.coroutine def async_service_handler(service): """Map services to methods on MediaPlayerDevice.""" @@ -392,6 +411,8 @@ def async_service_handler(service): params['position'] = service.data.get(ATTR_MEDIA_SEEK_POSITION) elif service.service == SERVICE_SELECT_SOURCE: params['source'] = service.data.get(ATTR_INPUT_SOURCE) + elif service.service == SERVICE_SELECT_SOUND_MODE: + params['sound_mode'] = service.data.get(ATTR_SOUND_MODE) elif service.service == SERVICE_PLAY_MEDIA: params['media_type'] = \ service.data.get(ATTR_MEDIA_CONTENT_TYPE) @@ -418,7 +439,7 @@ def async_service_handler(service): 'schema', MEDIA_PLAYER_SCHEMA) hass.services.async_register( DOMAIN, service, async_service_handler, - descriptions.get(service), schema=schema) + schema=schema) return True @@ -574,6 +595,16 @@ def source_list(self): """List of available input sources.""" return None + @property + def sound_mode(self): + """Name of the current sound mode.""" + return None + + @property + def sound_mode_list(self): + """List of available sound modes.""" + return None + @property def shuffle(self): """Boolean if shuffle is enabled.""" @@ -717,6 +748,17 @@ def async_select_source(self, source): """ return self.hass.async_add_job(self.select_source, source) + def select_sound_mode(self, sound_mode): + """Select sound mode.""" + raise NotImplementedError() + + def async_select_sound_mode(self, sound_mode): + """Select sound mode. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(self.select_sound_mode, sound_mode) + def clear_playlist(self): """Clear players playlist.""" raise NotImplementedError() @@ -790,6 +832,11 @@ def support_select_source(self): """Boolean if select source command supported.""" return bool(self.supported_features & SUPPORT_SELECT_SOURCE) + @property + def support_select_sound_mode(self): + """Boolean if select sound mode command supported.""" + return bool(self.supported_features & SUPPORT_SELECT_SOUND_MODE) + @property def support_clear_playlist(self): """Boolean if clear playlist command supported.""" @@ -935,14 +982,14 @@ class MediaPlayerImageView(HomeAssistantView): url = '/api/media_player_proxy/{entity_id}' name = 'api:media_player:image' - def __init__(self, entities): + def __init__(self, component): """Initialize a media player view.""" - self.entities = entities + self.component = component @asyncio.coroutine def get(self, request, entity_id): """Start a get request.""" - player = self.entities.get(entity_id) + player = self.component.get_entity(entity_id) if player is None: status = 404 if request[KEY_AUTHENTICATED] else 401 return web.Response(status=status) diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index 0a03af0e1bf03..a009ece9f4b2b 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -6,15 +6,18 @@ """ import logging -from collections import namedtuple +from collections import (namedtuple, OrderedDict) +import urllib +import xml.etree.ElementTree as ET import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_PAUSE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL, - MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_TURN_ON, - MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET, SUPPORT_PLAY) + SUPPORT_SELECT_SOURCE, SUPPORT_SELECT_SOUND_MODE, + SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL, MediaPlayerDevice, + PLATFORM_SCHEMA, SUPPORT_TURN_ON, MEDIA_TYPE_MUSIC, + SUPPORT_VOLUME_SET, SUPPORT_PLAY) from homeassistant.const import ( CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED, CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT) @@ -27,8 +30,19 @@ DEFAULT_NAME = None DEFAULT_SHOW_SOURCES = False DEFAULT_TIMEOUT = 2 +DEFAULT_SOUND_MODE = True +DEFAULT_SOUND_MODE_DICT = OrderedDict([('MUSIC', 'PLII MUSIC'), + ('MOVIE', 'PLII MOVIE'), + ('GAME', 'PLII GAME'), + ('PURE DIRECT', 'DIRECT'), + ('AUTO', 'None'), + ('DOLBY DIGITAL', 'DOLBY DIGITAL'), + ('MCH STEREO', 'MULTI CH STEREO'), + ('STEREO', 'STEREO')]) CONF_SHOW_ALL_SOURCES = 'show_all_sources' CONF_ZONES = 'zones' +CONF_SOUND_MODE = 'sound_mode' +CONF_SOUND_MODE_DICT = 'sound_mode_dict' CONF_VALID_ZONES = ['Zone2', 'Zone3'] CONF_INVALID_ZONES_ERR = 'Invalid Zone (expected Zone2 or Zone3)' KEY_DENON_CACHE = 'denonavr_hosts' @@ -49,6 +63,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SOUND_MODE, default=DEFAULT_SOUND_MODE): cv.boolean, + vol.Optional(CONF_SOUND_MODE_DICT, + default=DEFAULT_SOUND_MODE_DICT): vol.Schema({str: str}), vol.Optional(CONF_SHOW_ALL_SOURCES, default=DEFAULT_SHOW_SOURCES): cv.boolean, vol.Optional(CONF_ZONES): @@ -74,6 +91,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # Get config option for show_all_sources and timeout show_all_sources = config.get(CONF_SHOW_ALL_SOURCES) timeout = config.get(CONF_TIMEOUT) + sound_mode_support = config.get(CONF_SOUND_MODE) + sound_mode_dict = config.get(CONF_SOUND_MODE_DICT) # Get config option for additional zones zones = config.get(CONF_ZONES) @@ -117,7 +136,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): show_all_inputs=show_all_sources, timeout=timeout, add_zones=add_zones) for new_zone in new_device.zones.values(): - receivers.append(DenonDevice(new_zone)) + receivers.append(DenonDevice(new_zone, host, + sound_mode_support, + sound_mode_dict)) cache.add(host) _LOGGER.info("Denon receiver at host %s initialized", host) @@ -129,14 +150,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DenonDevice(MediaPlayerDevice): """Representation of a Denon Media Player Device.""" - def __init__(self, receiver): + def __init__(self, receiver, host, sound_mode_support, sound_mode_dict): """Initialize the device.""" self._receiver = receiver self._name = self._receiver.name + self._host = host self._muted = self._receiver.muted self._volume = self._receiver.volume self._current_source = self._receiver.input_func self._source_list = self._receiver.input_func_list + self._current_sound_mode = None + self._sound_mode_list = list(sound_mode_dict) + self._sound_mode_dict = sound_mode_dict + self._sound_mode_support = sound_mode_support self._state = self._receiver.state self._power = self._receiver.power self._media_image_url = self._receiver.image_url @@ -147,6 +173,10 @@ def __init__(self, receiver): self._frequency = self._receiver.frequency self._station = self._receiver.station + self._supported_features_base = SUPPORT_DENON + self._supported_features_base |= (sound_mode_support and + SUPPORT_SELECT_SOUND_MODE) + def update(self): """Get the latest status information from device.""" self._receiver.update() @@ -165,6 +195,26 @@ def update(self): self._frequency = self._receiver.frequency self._station = self._receiver.station + if self._sound_mode_support: + try: + url = ('http://' + str(self._host) + + '/goform/formMainZone_MainZoneXml.xml') + xml_data = urllib.request.urlopen(url) + except urllib.error.URLError: + err = "Denon receiver failed to get sound mode, URL-Error" + _LOGGER.error(err) + return + parsed_data = ET.parse(xml_data).getroot() + sound_mode_raw = parsed_data.find('selectSurround/value') + sound_mode_raw = sound_mode_raw.text.rstrip() + try: + mode_list = list(self._sound_mode_dict.values()) + mode_index = mode_list.index(sound_mode_raw.upper()) + sound_mode = list(self._sound_mode_dict.keys())[mode_index] + self._current_sound_mode = sound_mode + except ValueError: + self._current_sound_mode = sound_mode_raw + @property def name(self): """Return the name of the device.""" @@ -197,12 +247,22 @@ def source_list(self): """Return a list of available input sources.""" return self._source_list + @property + def sound_mode(self): + """Return the current sound mode.""" + return self._current_sound_mode + + @property + def sound_mode_list(self): + """Return a list of available sound modes.""" + return self._sound_mode_list + @property def supported_features(self): """Flag media player features that are supported.""" if self._current_source in self._receiver.netaudio_func_list: - return SUPPORT_DENON | SUPPORT_MEDIA_MODES - return SUPPORT_DENON + return self._supported_features_base | SUPPORT_MEDIA_MODES + return self._supported_features_base @property def media_content_id(self): @@ -292,6 +352,14 @@ def select_source(self, source): """Select input source.""" return self._receiver.set_input_func(source) + def select_sound_mode(self, sound_mode): + """Select sound mode.""" + url = ('http://' + str(self._host) + + '/MainZone/index.put.asp?cmd0=PutSurroundMode%2F' + + sound_mode.upper().replace(" ", "+")) + urllib.request.urlopen(url) + return sound_mode + def turn_on(self): """Turn on media player.""" if self._receiver.power_on(): diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index b2f98d378cf27..7d5700b0c2759 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -107,6 +107,20 @@ media_seek: description: Position to seek to. The format is platform dependent. example: 100 +monoprice_snapshot: + description: Take a snapshot of the media player zone. + fields: + entity_id: + description: Name(s) of entities that will be snapshot. Platform dependent. + example: 'media_player.living_room' + +monoprice_restore: + description: Restore a snapshot of the media player zone. + fields: + entity_id: + description: Name(s) of entities that will be restored. Platform dependent. + example: 'media_player.living_room' + play_media: description: Send the media player the command for playing media. fields: @@ -130,6 +144,16 @@ select_source: description: Name of the source to switch to. Platform dependent. example: 'video1' +select_sound_mode: + description: Send the media player the command to change sound mode. + fields: + entity_id: + description: Name(s) of entities to change sound mode on. + example: 'media_player.marantz' + sound_mode: + description: Name of the sound mode to switch to. + example: 'Music' + clear_playlist: description: Send the media player the command to clear players playlist. fields: @@ -293,3 +317,30 @@ kodi_call_method: method: description: Name of the Kodi JSONRPC API method to be called. example: 'VideoLibrary.GetRecentlyAddedEpisodes' + +squeezebox_call_method: + description: 'Call a Squeezebox JSON/RPC API method.' + fields: + entity_id: + description: Name(s) of the Squeexebox entities where to run the API method. + example: 'media_player.squeezebox_radio' + command: + description: Name of the Squeezebox command. + example: 'playlist' + parameters: + description: Optional array of parameters to be appended to the command. See 'Command Line Interface' official help page from Logitech for details. + example: '["loadtracks", "track.titlesearch=highway to hell"]' + +yamaha_enable_output: + description: Enable or disable an output port + + fields: + entity_id: + description: Name(s) of entites to enable/disable port on. + example: 'media_player.yamaha' + port: + description: Name of port to enable/disable. + example: 'hdmi1' + enabled: + description: Boolean indicating if port should be enabled or not. + example: true