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 devicepull/11827/head
parent
4ee2c311a7
commit
8703124c76
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
if receiver.zone in zone_ignore:
|
||||
continue
|
||||
|
||||
device = YamahaDevice(name, receiver, source_ignore,
|
||||
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):
|
||||
|
@ -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):
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue