Add enable_output service to Yamaha platform (#11103)

* Add enable_output service to Yamaha platform

* Fix lint issues

* Fix review comment

* Check entity_ids instead of device
This commit is contained in:
Pierre Ståhl 2018-01-17 19:34:21 +01:00 committed by Martin Hjelmare
parent 4ee2c311a7
commit 8703124c76
4 changed files with 160 additions and 3591 deletions

View File

@ -320,3 +320,17 @@ squeezebox_call_method:
parameters: parameters:
description: Optional array of parameters to be appended to the command. See 'Command Line Interface' official help page from Logitech for details. 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"]' 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

View File

@ -12,10 +12,10 @@ from homeassistant.components.media_player import (
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, SUPPORT_PAUSE, SUPPORT_STOP,
SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_MUSIC, MEDIA_PLAYER_SCHEMA, DOMAIN,
MediaPlayerDevice, PLATFORM_SCHEMA) MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON, from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON,
STATE_PLAYING, STATE_IDLE) STATE_PLAYING, STATE_IDLE, ATTR_ENTITY_ID)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['rxv==0.5.1'] REQUIREMENTS = ['rxv==0.5.1']
@ -31,7 +31,7 @@ CONF_ZONE_NAMES = 'zone_names'
CONF_ZONE_IGNORE = 'zone_ignore' CONF_ZONE_IGNORE = 'zone_ignore'
DEFAULT_NAME = 'Yamaha Receiver' DEFAULT_NAME = 'Yamaha Receiver'
KNOWN = 'yamaha_known_receivers' DATA_YAMAHA = 'yamaha_known_receivers'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@ -44,15 +44,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_ZONE_NAMES, default={}): {cv.string: cv.string}, vol.Optional(CONF_ZONE_NAMES, default={}): {cv.string: cv.string},
}) })
SERVICE_ENABLE_OUTPUT = 'yamaha_enable_output'
ATTR_PORT = 'port'
ATTR_ENABLED = 'enabled'
ENABLE_OUTPUT_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_PORT): cv.string,
vol.Required(ATTR_ENABLED): cv.boolean
})
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Yamaha platform.""" """Set up the Yamaha platform."""
import rxv import rxv
# keep track of configured receivers so that we don't end up # Keep track of configured receivers so that we don't end up
# discovering a receiver dynamically that we have static config # discovering a receiver dynamically that we have static config
# for. # for. Map each device from its unique_id to an instance since
if hass.data.get(KNOWN, None) is None: # YamahaDevice is not hashable (thus not possible to add to a set).
hass.data[KNOWN] = set() if hass.data.get(DATA_YAMAHA) is None:
hass.data[DATA_YAMAHA] = {}
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
@ -66,9 +77,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
model = discovery_info.get('model_name') model = discovery_info.get('model_name')
ctrl_url = discovery_info.get('control_url') ctrl_url = discovery_info.get('control_url')
desc_url = discovery_info.get('description_url') desc_url = discovery_info.get('description_url')
if ctrl_url in hass.data[KNOWN]:
_LOGGER.info("%s already manually configured", ctrl_url)
return
receivers = rxv.RXV( receivers = rxv.RXV(
ctrl_url, model_name=model, friendly_name=name, ctrl_url, model_name=model, friendly_name=name,
unit_desc_url=desc_url).zone_controllers() unit_desc_url=desc_url).zone_controllers()
@ -83,13 +91,40 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
ctrl_url = "http://{}:80/YamahaRemoteControl/ctrl".format(host) ctrl_url = "http://{}:80/YamahaRemoteControl/ctrl".format(host)
receivers = rxv.RXV(ctrl_url, name).zone_controllers() receivers = rxv.RXV(ctrl_url, name).zone_controllers()
devices = []
for receiver in receivers: for receiver in receivers:
if receiver.zone not in zone_ignore: if receiver.zone in zone_ignore:
hass.data[KNOWN].add(receiver.ctrl_url) continue
add_devices([
YamahaDevice(name, receiver, source_ignore, device = YamahaDevice(name, receiver, source_ignore,
source_names, zone_names) source_names, zone_names)
], True)
# Only add device if it's not already added
if device.unique_id not in hass.data[DATA_YAMAHA]:
hass.data[DATA_YAMAHA][device.unique_id] = device
devices.append(device)
else:
_LOGGER.debug('Ignoring duplicate receiver %s', name)
def service_handler(service):
"""Handle for services."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
devices = [device for device in hass.data[DATA_YAMAHA].values()
if not entity_ids or device.entity_id in entity_ids]
for device in devices:
port = service.data[ATTR_PORT]
enabled = service.data[ATTR_ENABLED]
device.enable_output(port, enabled)
device.schedule_update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_ENABLE_OUTPUT, service_handler,
schema=ENABLE_OUTPUT_SCHEMA)
add_devices(devices)
class YamahaDevice(MediaPlayerDevice): class YamahaDevice(MediaPlayerDevice):
@ -98,7 +133,7 @@ class YamahaDevice(MediaPlayerDevice):
def __init__(self, name, receiver, source_ignore, def __init__(self, name, receiver, source_ignore,
source_names, zone_names): source_names, zone_names):
"""Initialize the Yamaha Receiver.""" """Initialize the Yamaha Receiver."""
self._receiver = receiver self.receiver = receiver
self._muted = False self._muted = False
self._volume = 0 self._volume = 0
self._pwstate = STATE_OFF self._pwstate = STATE_OFF
@ -114,10 +149,15 @@ class YamahaDevice(MediaPlayerDevice):
self._name = name self._name = name
self._zone = receiver.zone self._zone = receiver.zone
@property
def unique_id(self):
"""Return an unique ID."""
return '{0}:{1}'.format(self.receiver.ctrl_url, self._zone)
def update(self): def update(self):
"""Get the latest details from the device.""" """Get the latest details from the device."""
self._play_status = self._receiver.play_status() self._play_status = self.receiver.play_status()
if self._receiver.on: if self.receiver.on:
if self._play_status is None: if self._play_status is None:
self._pwstate = STATE_ON self._pwstate = STATE_ON
elif self._play_status.playing: elif self._play_status.playing:
@ -127,17 +167,17 @@ class YamahaDevice(MediaPlayerDevice):
else: else:
self._pwstate = STATE_OFF self._pwstate = STATE_OFF
self._muted = self._receiver.mute self._muted = self.receiver.mute
self._volume = (self._receiver.volume / 100) + 1 self._volume = (self.receiver.volume / 100) + 1
if self.source_list is None: if self.source_list is None:
self.build_source_list() self.build_source_list()
current_source = self._receiver.input current_source = self.receiver.input
self._current_source = self._source_names.get( self._current_source = self._source_names.get(
current_source, current_source) current_source, current_source)
self._playback_support = self._receiver.get_playback_support() self._playback_support = self.receiver.get_playback_support()
self._is_playback_supported = self._receiver.is_playback_supported( self._is_playback_supported = self.receiver.is_playback_supported(
self._current_source) self._current_source)
def build_source_list(self): def build_source_list(self):
@ -147,7 +187,7 @@ class YamahaDevice(MediaPlayerDevice):
self._source_list = sorted( self._source_list = sorted(
self._source_names.get(source, source) for source in self._source_names.get(source, source) for source in
self._receiver.inputs() self.receiver.inputs()
if source not in self._source_ignore) if source not in self._source_ignore)
@property @property
@ -203,42 +243,42 @@ class YamahaDevice(MediaPlayerDevice):
def turn_off(self): def turn_off(self):
"""Turn off media player.""" """Turn off media player."""
self._receiver.on = False self.receiver.on = False
def set_volume_level(self, volume): def set_volume_level(self, volume):
"""Set volume level, range 0..1.""" """Set volume level, range 0..1."""
receiver_vol = 100 - (volume * 100) receiver_vol = 100 - (volume * 100)
negative_receiver_vol = -receiver_vol negative_receiver_vol = -receiver_vol
self._receiver.volume = negative_receiver_vol self.receiver.volume = negative_receiver_vol
def mute_volume(self, mute): def mute_volume(self, mute):
"""Mute (true) or unmute (false) media player.""" """Mute (true) or unmute (false) media player."""
self._receiver.mute = mute self.receiver.mute = mute
def turn_on(self): def turn_on(self):
"""Turn the media player on.""" """Turn the media player on."""
self._receiver.on = True self.receiver.on = True
self._volume = (self._receiver.volume / 100) + 1 self._volume = (self.receiver.volume / 100) + 1
def media_play(self): def media_play(self):
"""Send play command.""" """Send play command."""
self._call_playback_function(self._receiver.play, "play") self._call_playback_function(self.receiver.play, "play")
def media_pause(self): def media_pause(self):
"""Send pause command.""" """Send pause command."""
self._call_playback_function(self._receiver.pause, "pause") self._call_playback_function(self.receiver.pause, "pause")
def media_stop(self): def media_stop(self):
"""Send stop command.""" """Send stop command."""
self._call_playback_function(self._receiver.stop, "stop") self._call_playback_function(self.receiver.stop, "stop")
def media_previous_track(self): def media_previous_track(self):
"""Send previous track command.""" """Send previous track command."""
self._call_playback_function(self._receiver.previous, "previous track") self._call_playback_function(self.receiver.previous, "previous track")
def media_next_track(self): def media_next_track(self):
"""Send next track command.""" """Send next track command."""
self._call_playback_function(self._receiver.next, "next track") self._call_playback_function(self.receiver.next, "next track")
def _call_playback_function(self, function, function_text): def _call_playback_function(self, function, function_text):
import rxv import rxv
@ -250,7 +290,7 @@ class YamahaDevice(MediaPlayerDevice):
def select_source(self, source): def select_source(self, source):
"""Select input source.""" """Select input source."""
self._receiver.input = self._reverse_mapping.get(source, source) self.receiver.input = self._reverse_mapping.get(source, source)
def play_media(self, media_type, media_id, **kwargs): def play_media(self, media_type, media_id, **kwargs):
"""Play media from an ID. """Play media from an ID.
@ -275,7 +315,11 @@ class YamahaDevice(MediaPlayerDevice):
""" """
if media_type == "NET RADIO": if media_type == "NET RADIO":
self._receiver.net_radio(media_id) self.receiver.net_radio(media_id)
def enable_output(self, port, enabled):
"""Enable or disable an output port.."""
self.receiver.enable_output(port, enabled)
@property @property
def media_artist(self): def media_artist(self):

View File

@ -1,129 +1,81 @@
"""The tests for the Yamaha Media player platform.""" """The tests for the Yamaha Media player platform."""
import unittest import unittest
import xml.etree.ElementTree as ET from unittest.mock import patch, MagicMock
import rxv from homeassistant.setup import setup_component
import homeassistant.components.media_player as mp
import homeassistant.components.media_player.yamaha as yamaha from homeassistant.components.media_player import yamaha
from tests.common import get_test_home_assistant
TEST_CONFIG = {
'name': "Test Receiver",
'source_ignore': ['HDMI5'],
'source_names': {'HDMI1': 'Laserdisc'},
'zone_names': {'Main_Zone': "Laser Dome"}
}
def sample_content(name): def _create_zone_mock(name, url):
"""Read content into a string from a file.""" zone = MagicMock()
with open('tests/components/media_player/yamaha_samples/%s' % name, zone.ctrl_url = url
encoding='utf-8') as content: zone.zone = name
return content.read() return zone
def yamaha_player(receiver): class FakeYamahaDevice(object):
"""Create a YamahaDevice from a given receiver, presumably a Mock.""" """A fake Yamaha device."""
zone_controller = receiver.zone_controllers()[0]
player = yamaha.YamahaDevice(receiver=zone_controller, **TEST_CONFIG) def __init__(self, ctrl_url, name, zones=None):
player.build_source_list() """Initialize the fake Yamaha device."""
return player self.ctrl_url = ctrl_url
self.name = name
self.zones = zones or []
def zone_controllers(self):
"""Return controllers for all available zones."""
return self.zones
class FakeYamaha(rxv.rxv.RXV): class TestYamahaMediaPlayer(unittest.TestCase):
"""Fake Yamaha receiver. """Test the Yamaha media player."""
This inherits from RXV but overrides methods for testing that
would normally have hit the network. This makes it easier to
ensure that usage of the rxv library by HomeAssistant is as we'd
expect.
"""
_fake_input = 'HDMI1'
def _discover_features(self):
"""Fake the discovery feature."""
self._desc_xml = ET.fromstring(sample_content('desc.xml'))
@property
def input(self):
"""A fake input for the receiver."""
return self._fake_input
@input.setter
def input(self, input_name):
"""Set the input for the fake receiver."""
assert input_name in self.inputs()
self._fake_input = input_name
def inputs(self):
"""All inputs of the fake receiver."""
return {'AUDIO1': None,
'AUDIO2': None,
'AV1': None,
'AV2': None,
'AV3': None,
'AV4': None,
'AV5': None,
'AV6': None,
'AirPlay': 'AirPlay',
'HDMI1': None,
'HDMI2': None,
'HDMI3': None,
'HDMI4': None,
'HDMI5': None,
'NET RADIO': 'NET_RADIO',
'Pandora': 'Pandora',
'Rhapsody': 'Rhapsody',
'SERVER': 'SERVER',
'SiriusXM': 'SiriusXM',
'Spotify': 'Spotify',
'TUNER': 'Tuner',
'USB': 'USB',
'V-AUX': None,
'iPod (USB)': 'iPod_USB'}
# pylint: disable=no-member, invalid-name
class TestYamaha(unittest.TestCase):
"""Test the media_player yamaha module."""
def setUp(self): def setUp(self):
"""Setup things to be run when tests are started.""" """Setup things to be run when tests are started."""
super(TestYamaha, self).setUp() self.hass = get_test_home_assistant()
self.rec = FakeYamaha("http://10.0.0.0:80/YamahaRemoteControl/ctrl") self.main_zone = _create_zone_mock('Main zone', 'http://main')
self.player = yamaha_player(self.rec) self.device = FakeYamahaDevice(
'http://receiver', 'Receiver', zones=[self.main_zone])
def test_get_playback_support(self): def tearDown(self):
"""Test the playback.""" """Stop everything that was started."""
rec = self.rec self.hass.stop()
support = rec.get_playback_support()
self.assertFalse(support.play)
self.assertFalse(support.pause)
self.assertFalse(support.stop)
self.assertFalse(support.skip_f)
self.assertFalse(support.skip_r)
rec.input = 'NET RADIO' def enable_output(self, port, enabled):
support = rec.get_playback_support() """Enable ouput on a specific port."""
self.assertTrue(support.play) data = {
self.assertFalse(support.pause) 'entity_id': 'media_player.yamaha_receiver_main_zone',
self.assertTrue(support.stop) 'port': port,
self.assertFalse(support.skip_f) 'enabled': enabled
self.assertFalse(support.skip_r) }
def test_configuration_options(self): self.hass.services.call(yamaha.DOMAIN,
"""Test configuration options.""" yamaha.SERVICE_ENABLE_OUTPUT,
rec_name = TEST_CONFIG['name'] data,
src_zone = 'Main_Zone' True)
src_zone_alt = src_zone.replace('_', ' ')
renamed_zone = TEST_CONFIG['zone_names'][src_zone]
ignored_src = TEST_CONFIG['source_ignore'][0]
renamed_src = 'HDMI1'
new_src = TEST_CONFIG['source_names'][renamed_src]
self.assertFalse(self.player.name == rec_name + ' ' + src_zone)
self.assertFalse(self.player.name == rec_name + ' ' + src_zone_alt)
self.assertTrue(self.player.name == rec_name + ' ' + renamed_zone)
self.assertFalse(ignored_src in self.player.source_list) def create_receiver(self, mock_rxv):
self.assertFalse(renamed_src in self.player.source_list) """Create a mocked receiver."""
self.assertTrue(new_src in self.player.source_list) mock_rxv.return_value = self.device
config = {
'media_player': {
'platform': 'yamaha',
'host': '127.0.0.1'
}
}
self.assertTrue(setup_component(self.hass, mp.DOMAIN, config))
@patch('rxv.RXV')
def test_enable_output(self, mock_rxv):
"""Test enabling and disabling outputs."""
self.create_receiver(mock_rxv)
self.enable_output('hdmi1', True)
self.main_zone.enable_output.assert_called_with('hdmi1', True)
self.enable_output('hdmi2', False)
self.main_zone.enable_output.assert_called_with('hdmi2', False)

File diff suppressed because it is too large Load Diff