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
pull/11827/head
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:
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

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_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, SUPPORT_PAUSE, SUPPORT_STOP,
SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY,
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_MUSIC, MEDIA_PLAYER_SCHEMA, DOMAIN,
MediaPlayerDevice, PLATFORM_SCHEMA)
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
REQUIREMENTS = ['rxv==0.5.1']
@ -31,7 +31,7 @@ CONF_ZONE_NAMES = 'zone_names'
CONF_ZONE_IGNORE = 'zone_ignore'
DEFAULT_NAME = 'Yamaha Receiver'
KNOWN = 'yamaha_known_receivers'
DATA_YAMAHA = 'yamaha_known_receivers'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
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},
})
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):
"""Set up the Yamaha platform."""
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
# for.
if hass.data.get(KNOWN, None) is None:
hass.data[KNOWN] = set()
# for. Map each device from its unique_id to an instance since
# YamahaDevice is not hashable (thus not possible to add to a set).
if hass.data.get(DATA_YAMAHA) is None:
hass.data[DATA_YAMAHA] = {}
name = config.get(CONF_NAME)
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')
ctrl_url = discovery_info.get('control_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(
ctrl_url, model_name=model, friendly_name=name,
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)
receivers = rxv.RXV(ctrl_url, name).zone_controllers()
devices = []
for receiver in receivers:
if receiver.zone not in zone_ignore:
hass.data[KNOWN].add(receiver.ctrl_url)
add_devices([
YamahaDevice(name, receiver, source_ignore,
source_names, zone_names)
], True)
if receiver.zone in zone_ignore:
continue
device = YamahaDevice(name, receiver, source_ignore,
source_names, zone_names)
# 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):
@ -98,7 +133,7 @@ class YamahaDevice(MediaPlayerDevice):
def __init__(self, name, receiver, source_ignore,
source_names, zone_names):
"""Initialize the Yamaha Receiver."""
self._receiver = receiver
self.receiver = receiver
self._muted = False
self._volume = 0
self._pwstate = STATE_OFF
@ -114,10 +149,15 @@ class YamahaDevice(MediaPlayerDevice):
self._name = name
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):
"""Get the latest details from the device."""
self._play_status = self._receiver.play_status()
if self._receiver.on:
self._play_status = self.receiver.play_status()
if self.receiver.on:
if self._play_status is None:
self._pwstate = STATE_ON
elif self._play_status.playing:
@ -127,17 +167,17 @@ class YamahaDevice(MediaPlayerDevice):
else:
self._pwstate = STATE_OFF
self._muted = self._receiver.mute
self._volume = (self._receiver.volume / 100) + 1
self._muted = self.receiver.mute
self._volume = (self.receiver.volume / 100) + 1
if self.source_list is None:
self.build_source_list()
current_source = self._receiver.input
current_source = self.receiver.input
self._current_source = self._source_names.get(
current_source, current_source)
self._playback_support = self._receiver.get_playback_support()
self._is_playback_supported = self._receiver.is_playback_supported(
self._playback_support = self.receiver.get_playback_support()
self._is_playback_supported = self.receiver.is_playback_supported(
self._current_source)
def build_source_list(self):
@ -147,7 +187,7 @@ class YamahaDevice(MediaPlayerDevice):
self._source_list = sorted(
self._source_names.get(source, source) for source in
self._receiver.inputs()
self.receiver.inputs()
if source not in self._source_ignore)
@property
@ -203,42 +243,42 @@ class YamahaDevice(MediaPlayerDevice):
def turn_off(self):
"""Turn off media player."""
self._receiver.on = False
self.receiver.on = False
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
receiver_vol = 100 - (volume * 100)
negative_receiver_vol = -receiver_vol
self._receiver.volume = negative_receiver_vol
self.receiver.volume = negative_receiver_vol
def mute_volume(self, mute):
"""Mute (true) or unmute (false) media player."""
self._receiver.mute = mute
self.receiver.mute = mute
def turn_on(self):
"""Turn the media player on."""
self._receiver.on = True
self._volume = (self._receiver.volume / 100) + 1
self.receiver.on = True
self._volume = (self.receiver.volume / 100) + 1
def media_play(self):
"""Send play command."""
self._call_playback_function(self._receiver.play, "play")
self._call_playback_function(self.receiver.play, "play")
def media_pause(self):
"""Send pause command."""
self._call_playback_function(self._receiver.pause, "pause")
self._call_playback_function(self.receiver.pause, "pause")
def media_stop(self):
"""Send stop command."""
self._call_playback_function(self._receiver.stop, "stop")
self._call_playback_function(self.receiver.stop, "stop")
def media_previous_track(self):
"""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):
"""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):
import rxv
@ -250,7 +290,7 @@ class YamahaDevice(MediaPlayerDevice):
def select_source(self, 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):
"""Play media from an ID.
@ -275,7 +315,11 @@ class YamahaDevice(MediaPlayerDevice):
"""
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
def media_artist(self):

View File

@ -1,129 +1,81 @@
"""The tests for the Yamaha Media player platform."""
import unittest
import xml.etree.ElementTree as ET
from unittest.mock import patch, MagicMock
import rxv
import homeassistant.components.media_player.yamaha as yamaha
TEST_CONFIG = {
'name': "Test Receiver",
'source_ignore': ['HDMI5'],
'source_names': {'HDMI1': 'Laserdisc'},
'zone_names': {'Main_Zone': "Laser Dome"}
}
from homeassistant.setup import setup_component
import homeassistant.components.media_player as mp
from homeassistant.components.media_player import yamaha
from tests.common import get_test_home_assistant
def sample_content(name):
"""Read content into a string from a file."""
with open('tests/components/media_player/yamaha_samples/%s' % name,
encoding='utf-8') as content:
return content.read()
def _create_zone_mock(name, url):
zone = MagicMock()
zone.ctrl_url = url
zone.zone = name
return zone
def yamaha_player(receiver):
"""Create a YamahaDevice from a given receiver, presumably a Mock."""
zone_controller = receiver.zone_controllers()[0]
player = yamaha.YamahaDevice(receiver=zone_controller, **TEST_CONFIG)
player.build_source_list()
return player
class FakeYamahaDevice(object):
"""A fake Yamaha device."""
def __init__(self, ctrl_url, name, zones=None):
"""Initialize the fake Yamaha device."""
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):
"""Fake Yamaha receiver.
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."""
class TestYamahaMediaPlayer(unittest.TestCase):
"""Test the Yamaha media player."""
def setUp(self):
"""Setup things to be run when tests are started."""
super(TestYamaha, self).setUp()
self.rec = FakeYamaha("http://10.0.0.0:80/YamahaRemoteControl/ctrl")
self.player = yamaha_player(self.rec)
self.hass = get_test_home_assistant()
self.main_zone = _create_zone_mock('Main zone', 'http://main')
self.device = FakeYamahaDevice(
'http://receiver', 'Receiver', zones=[self.main_zone])
def test_get_playback_support(self):
"""Test the playback."""
rec = self.rec
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)
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
rec.input = 'NET RADIO'
support = rec.get_playback_support()
self.assertTrue(support.play)
self.assertFalse(support.pause)
self.assertTrue(support.stop)
self.assertFalse(support.skip_f)
self.assertFalse(support.skip_r)
def enable_output(self, port, enabled):
"""Enable ouput on a specific port."""
data = {
'entity_id': 'media_player.yamaha_receiver_main_zone',
'port': port,
'enabled': enabled
}
def test_configuration_options(self):
"""Test configuration options."""
rec_name = TEST_CONFIG['name']
src_zone = 'Main_Zone'
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.hass.services.call(yamaha.DOMAIN,
yamaha.SERVICE_ENABLE_OUTPUT,
data,
True)
self.assertFalse(ignored_src in self.player.source_list)
self.assertFalse(renamed_src in self.player.source_list)
self.assertTrue(new_src in self.player.source_list)
def create_receiver(self, mock_rxv):
"""Create a mocked receiver."""
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