Add onkyo receiver, and source select support

Added onkyo receiver component

Added support for input source selection to media players,
and do the onkyo receiver component.
This commit is contained in:
Daniel J. Kemp 2016-03-27 14:41:52 -04:00
parent 79a2d40f4d
commit 86199c8277
10 changed files with 254 additions and 4 deletions

View File

@ -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

View File

@ -34,6 +34,7 @@ DISCOVERY_PLATFORMS = {
}
SERVICE_PLAY_MEDIA = 'play_media'
SERVICE_SELECT_SOURCE = 'select_source'
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
@ -54,6 +55,7 @@ ATTR_MEDIA_PLAYLIST = 'media_playlist'
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'
@ -73,6 +75,7 @@ SUPPORT_TURN_ON = 128
SUPPORT_TURN_OFF = 256
SUPPORT_PLAY_MEDIA = 512
SUPPORT_VOLUME_STEP = 1024
SUPPORT_SELECT_SOURCE = 2048
SERVICE_TO_METHOD = {
SERVICE_TURN_ON: 'turn_on',
@ -86,6 +89,7 @@ SERVICE_TO_METHOD = {
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 = [
@ -107,6 +111,7 @@ ATTR_TO_PROPERTY = [
ATTR_APP_ID,
ATTR_APP_NAME,
ATTR_SUPPORTED_MEDIA_COMMANDS,
ATTR_INPUT_SOURCE,
]
@ -219,6 +224,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(
@ -301,6 +316,26 @@ def setup(hass, config):
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)
@ -429,6 +464,11 @@ class MediaPlayerDevice(Entity):
"""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."""
@ -474,6 +514,10 @@ class MediaPlayerDevice(Entity):
"""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):
@ -510,6 +554,11 @@ class MediaPlayerDevice(Entity):
"""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]:

View File

@ -8,7 +8,7 @@ from homeassistant.components.media_player import (
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 @@ MUSIC_PLAYER_SUPPORT = \
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 @@ class DemoTVShowPlayer(AbstractDemoPlayer):
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

View File

@ -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))

View File

@ -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'

View File

@ -18,7 +18,8 @@ from homeassistant.components.media_player import (
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 @@ class UniversalMediaPlayer(MediaPlayerDevice):
"""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 @@ class UniversalMediaPlayer(MediaPlayerDevice):
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 @@ class UniversalMediaPlayer(MediaPlayerDevice):
"""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:

View File

@ -6,7 +6,8 @@ from collections import defaultdict
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_ATTRIBUTES = {
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.

View File

@ -81,6 +81,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.uber
https://github.com/denismakogon/rides-python-sdk/archive/py3-support.zip#uber_rides==0.1.2-dev

View File

@ -19,6 +19,25 @@ class TestDemoMediaPlayer(unittest.TestCase):
"""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'}})

View File

@ -24,6 +24,7 @@ class MockMediaPlayer(media_player.MediaPlayerDevice):
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 @@ class MockMediaPlayer(media_player.MediaPlayerDevice):
'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 @@ class MockMediaPlayer(media_player.MediaPlayerDevice):
"""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 @@ class TestMediaPlayer(unittest.TestCase):
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