diff --git a/.coveragerc b/.coveragerc index 3656ad8fde5f0..448764d202c3c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -107,6 +107,7 @@ omit = homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/sonos.py homeassistant/components/media_player/squeezebox.py + homeassistant/components/media_player/onkyo.py homeassistant/components/media_player/yamaha.py homeassistant/components/notify/free_mobile.py homeassistant/components/notify/googlevoice.py diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index a2f4b7c528389..47ae1745fae26 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -35,6 +35,7 @@ } SERVICE_PLAY_MEDIA = 'play_media' +SERVICE_SELECT_SOURCE = 'select_source' ATTR_MEDIA_VOLUME_LEVEL = 'volume_level' ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted' @@ -55,6 +56,7 @@ ATTR_APP_ID = 'app_id' ATTR_APP_NAME = 'app_name' ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands' +ATTR_INPUT_SOURCE = 'source' MEDIA_TYPE_MUSIC = 'music' MEDIA_TYPE_TVSHOW = 'tvshow' @@ -74,6 +76,7 @@ SUPPORT_TURN_OFF = 256 SUPPORT_PLAY_MEDIA = 512 SUPPORT_VOLUME_STEP = 1024 +SUPPORT_SELECT_SOURCE = 2048 SERVICE_TO_METHOD = { SERVICE_TURN_ON: 'turn_on', @@ -87,6 +90,7 @@ SERVICE_MEDIA_NEXT_TRACK: 'media_next_track', SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track', SERVICE_PLAY_MEDIA: 'play_media', + SERVICE_SELECT_SOURCE: 'select_source', } ATTR_TO_PROPERTY = [ @@ -108,6 +112,7 @@ ATTR_APP_ID, ATTR_APP_NAME, ATTR_SUPPORTED_MEDIA_COMMANDS, + ATTR_INPUT_SOURCE, ] @@ -220,6 +225,16 @@ def play_media(hass, media_type, media_id, entity_id=None): hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data) +def select_source(hass, source, entity_id=None): + """Send the media player the command to select input source.""" + data = {ATTR_INPUT_SOURCE: source} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SELECT_SOURCE, data) + + def setup(hass, config): """Track states and offer events for media_players.""" component = EntityComponent( @@ -302,6 +317,26 @@ def media_seek_service(service): hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service, descriptions.get(SERVICE_MEDIA_SEEK)) + def select_source_service(service): + """Change input to selected source.""" + input_source = service.data.get(ATTR_INPUT_SOURCE) + + if input_source is None: + _LOGGER.error( + 'Received call to %s without attribute %s', + service.service, ATTR_INPUT_SOURCE) + return + + for player in component.extract_from_service(service): + player.select_source(input_source) + + if player.should_poll: + player.update_ha_state(True) + + hass.services.register(DOMAIN, SERVICE_SELECT_SOURCE, + select_source_service, + descriptions.get(SERVICE_SELECT_SOURCE)) + def play_media_service(service): """Play specified media_id on the media player.""" media_type = service.data.get(ATTR_MEDIA_CONTENT_TYPE) @@ -430,6 +465,11 @@ def app_name(self): """Name of the current running app.""" return None + @property + def source(self): + """Name of the current input source.""" + return None + @property def supported_media_commands(self): """Flag media commands that are supported.""" @@ -475,6 +515,10 @@ def play_media(self, media_type, media_id): """Play a piece of media.""" raise NotImplementedError() + def select_source(self, source): + """Select input source.""" + raise NotImplementedError() + # No need to overwrite these. @property def support_pause(self): @@ -511,6 +555,11 @@ def support_play_media(self): """Boolean if play media command supported.""" return bool(self.supported_media_commands & SUPPORT_PLAY_MEDIA) + @property + def support_select_source(self): + """Boolean if select source command supported.""" + return bool(self.supported_media_commands & SUPPORT_SELECT_SOURCE) + def toggle(self): """Toggle the power on the media player.""" if self.state in [STATE_OFF, STATE_IDLE]: diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index a433fe4d7aba8..c8a799aa9a92e 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -8,7 +8,7 @@ MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + SUPPORT_SELECT_SOURCE, MediaPlayerDevice) from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING @@ -20,7 +20,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): 'Living Room', 'eyU3bRy2x44', '♥♥ The Best Fireplace Video (3 hours)'), DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours'), - DemoMusicPlayer(), DemoTVShowPlayer(), + DemoMusicPlayer(), DemoTVShowPlayer(), DemoReceiver(), ]) @@ -37,6 +37,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): NETFLIX_PLAYER_SUPPORT = \ SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF +RECEIVER_SUPPORT = SUPPORT_SELECT_SOURCE + class AbstractDemoPlayer(MediaPlayerDevice): """A demo media players.""" @@ -337,3 +339,29 @@ def media_next_track(self): if self._cur_episode < self._episode_count: self._cur_episode += 1 self.update_ha_state() + + +class DemoReceiver(AbstractDemoPlayer): + """A Demo receiver that only supports changing source input.""" + + # We only implement the methods that we support + # pylint: disable=abstract-method + def __init__(self): + """Initialize the demo device.""" + super().__init__('receiver') + self._source = 'dvd' + + @property + def source(self): + """Return the current input source.""" + return self._source + + def select_source(self, source): + """Set the input source.""" + self._source = source + self.update_ha_state() + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return RECEIVER_SUPPORT diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py new file mode 100644 index 0000000000000..7810ac6344496 --- /dev/null +++ b/homeassistant/components/media_player/onkyo.py @@ -0,0 +1,111 @@ +""" +Support for Onkyo Receivers. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.onkyo/ +""" +import logging + +from homeassistant.components.media_player import ( + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_SELECT_SOURCE, MediaPlayerDevice) +from homeassistant.const import STATE_OFF, STATE_ON + +REQUIREMENTS = ['https://github.com/danieljkemp/onkyo-eiscp/archive/' + 'python3.zip#onkyo-eiscp==0.9.2'] +_LOGGER = logging.getLogger(__name__) + +SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Onkyo platform.""" + from eiscp import eISCP + add_devices(OnkyoDevice(receiver) + for receiver in eISCP.discover()) + + +class OnkyoDevice(MediaPlayerDevice): + """Representation of a Onkyo device.""" + + # pylint: disable=too-many-public-methods, abstract-method + def __init__(self, receiver): + """Initialize the Onkyo Receiver.""" + self._receiver = receiver + self._muted = False + self._volume = 0 + self._pwstate = STATE_OFF + self.update() + self._name = '{}_{}'.format( + receiver.info['model_name'], receiver.info['identifier']) + self._current_source = None + + def update(self): + """Get the latest details from the device.""" + status = self._receiver.command('system-power query') + if status[1] == 'on': + self._pwstate = STATE_ON + else: + self._pwstate = STATE_OFF + return + volume_raw = self._receiver.command('volume query') + mute_raw = self._receiver.command('audio-muting query') + current_source_raw = self._receiver.command('input-selector query') + self._current_source = '_'.join('_'.join( + [i for i in current_source_raw[1]])) + self._muted = bool(mute_raw[1] == 'on') + self._volume = int(volume_raw[1], 16)/80.0 + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._pwstate + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_ONKYO + + @property + def source(self): + """"Return the current input source of the device.""" + return self._current_source + + def turn_off(self): + """Turn off media player.""" + self._receiver.command('system-power standby') + + def set_volume_level(self, volume): + """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" + self._receiver.command('volume {}'.format(int(volume*80))) + + def mute_volume(self, mute): + """Mute (true) or unmute (false) media player.""" + if mute: + self._receiver.command('audio-muting on') + else: + self._receiver.command('audio-muting off') + + def turn_on(self): + """Turn the media player on.""" + self._receiver.power_on() + + def select_source(self, source): + """Set the input source.""" + self._receiver.command('input-selector {}'.format(source)) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 13c53e150a1a5..59227eda2a557 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -126,3 +126,14 @@ play_media: media_content_type: description: The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST example: 'MUSIC' + +select_source: + description: Send the media player the command to change input source. + + fields: + entity_id: + description: Name(s) of entites to change source on + example: 'media_player.media_player.txnr535_0009b0d81f82' + source: + description: Name of the source to switch to. Platform dependent. + example: 'video1' diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 0c37a37232815..7f25553248b94 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -18,7 +18,8 @@ ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_SUPPORTED_MEDIA_COMMANDS, DOMAIN, SERVICE_PLAY_MEDIA, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, ATTR_INPUT_SOURCE, + SERVICE_SELECT_SOURCE, MediaPlayerDevice) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, CONF_NAME, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, @@ -321,6 +322,11 @@ def app_name(self): """Name of the current running app.""" return self._child_attr(ATTR_APP_NAME) + @property + def current_source(self): + """"Return the current input source of the device.""" + return self._child_attr(ATTR_INPUT_SOURCE) + @property def supported_media_commands(self): """Flag media commands that are supported.""" @@ -340,6 +346,9 @@ def supported_media_commands(self): ATTR_MEDIA_VOLUME_MUTED in self._attrs: flags |= SUPPORT_VOLUME_MUTE + if SUPPORT_SELECT_SOURCE in self._cmds: + flags |= SUPPORT_SELECT_SOURCE + return flags @property @@ -406,6 +415,11 @@ def media_play_pause(self): """Play or pause the media player.""" self._call_service(SERVICE_MEDIA_PLAY_PAUSE) + def select_source(self, source): + """Set the input source.""" + data = {ATTR_INPUT_SOURCE: source} + self._call_service(SERVICE_SELECT_SOURCE, data) + def update(self): """Update state in HA.""" for child_name in self._children: diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 8d9f62b644de7..83db4a194b6eb 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -6,7 +6,8 @@ import homeassistant.util.dt as dt_util from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_SEEK_POSITION, - ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, SERVICE_PLAY_MEDIA) + ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, SERVICE_PLAY_MEDIA, + SERVICE_SELECT_SOURCE, ATTR_INPUT_SOURCE) from homeassistant.components.notify import ( ATTR_MESSAGE, SERVICE_NOTIFY) from homeassistant.components.sun import ( @@ -42,6 +43,7 @@ SERVICE_SET_AWAY_MODE: [ATTR_AWAY_MODE], SERVICE_SET_FAN_MODE: [ATTR_FAN], SERVICE_SET_TEMPERATURE: [ATTR_TEMPERATURE], + SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE], } # Update this dict when new services are added to HA. diff --git a/requirements_all.txt b/requirements_all.txt index 8c23d3b11e85f..e298be83998a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -82,6 +82,9 @@ https://github.com/Xorso/pyalarmdotcom/archive/0.1.1.zip#pyalarmdotcom==0.1.1 # homeassistant.components.modbus https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0 +# homeassistant.components.media_player.onkyo +https://github.com/danieljkemp/onkyo-eiscp/archive/python3.zip#onkyo-eiscp==0.9.2 + # homeassistant.components.sensor.sabnzbd https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1 diff --git a/tests/components/media_player/test_demo.py b/tests/components/media_player/test_demo.py index 7f4dc4e37c487..5bd9799c57861 100644 --- a/tests/components/media_player/test_demo.py +++ b/tests/components/media_player/test_demo.py @@ -19,6 +19,25 @@ def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" self.hass.stop() + def test_source_select(self): + """Test the input source service.""" + + entity_id = 'media_player.receiver' + + assert mp.setup(self.hass, {'media_player': {'platform': 'demo'}}) + state = self.hass.states.get(entity_id) + assert 'dvd' == state.attributes.get('source') + + mp.select_source(self.hass, None, entity_id) + self.hass.pool.block_till_done() + state = self.hass.states.get(entity_id) + assert 'dvd' == state.attributes.get('source') + + mp.select_source(self.hass, 'xbox', entity_id) + self.hass.pool.block_till_done() + state = self.hass.states.get(entity_id) + assert 'xbox' == state.attributes.get('source') + def test_volume_services(self): """Test the volume service.""" assert mp.setup(self.hass, {'media_player': {'platform': 'demo'}}) diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py index f7b86d42e5fa2..a6de09a3eb1ab 100644 --- a/tests/components/media_player/test_universal.py +++ b/tests/components/media_player/test_universal.py @@ -24,6 +24,7 @@ def __init__(self, hass, name): self._is_volume_muted = False self._media_title = None self._supported_media_commands = 0 + self._source = None self.service_calls = { 'turn_on': mock_service( @@ -55,6 +56,9 @@ def __init__(self, hass, name): 'media_play_pause': mock_service( hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PLAY_PAUSE), + 'select_source': mock_service( + hass, media_player.DOMAIN, + media_player.SERVICE_SELECT_SOURCE), } @property @@ -106,6 +110,10 @@ def media_pause(self): """Mock pause.""" self._state = STATE_PAUSED + def select_source(self, source): + """Set the input source.""" + self._state = source + class TestMediaPlayer(unittest.TestCase): """Test the media_player module.""" @@ -498,6 +506,10 @@ def test_service_call_to_child(self): self.assertEqual( 1, len(self.mock_mp_2.service_calls['media_play_pause'])) + ump.select_source('dvd') + self.assertEqual( + 1, len(self.mock_mp_2.service_calls['select_source'])) + def test_service_call_to_command(self): """Test service call to command.""" config = self.config_children_only