Skip to content
Closed
69 changes: 58 additions & 11 deletions homeassistant/components/media_player/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import collections
import hashlib
import logging
import os
from random import SystemRandom

from aiohttp import web
Expand All @@ -19,7 +18,6 @@
import voluptuous as vol

from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, ATTR_ENTITY_ID,
SERVICE_TOGGLE, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP,
Expand Down Expand Up @@ -57,6 +55,7 @@

SERVICE_PLAY_MEDIA = 'play_media'
SERVICE_SELECT_SOURCE = 'select_source'
SERVICE_SELECT_SOUND_MODE = 'select_sound_mode'
SERVICE_CLEAR_PLAYLIST = 'clear_playlist'

ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
Expand All @@ -81,6 +80,8 @@
ATTR_APP_NAME = 'app_name'
ATTR_INPUT_SOURCE = 'source'
ATTR_INPUT_SOURCE_LIST = 'source_list'
ATTR_SOUND_MODE = 'sound_mode'
ATTR_SOUND_MODE_LIST = 'sound_mode_list'
ATTR_MEDIA_ENQUEUE = 'enqueue'
ATTR_MEDIA_SHUFFLE = 'shuffle'

Expand All @@ -90,6 +91,7 @@
MEDIA_TYPE_EPISODE = 'episode'
MEDIA_TYPE_CHANNEL = 'channel'
MEDIA_TYPE_PLAYLIST = 'playlist'
MEDIA_TYPE_URL = 'url'

SUPPORT_PAUSE = 1
SUPPORT_SEEK = 2
Expand All @@ -107,6 +109,7 @@
SUPPORT_CLEAR_PLAYLIST = 8192
SUPPORT_PLAY = 16384
SUPPORT_SHUFFLE_SET = 32768
SUPPORT_SELECT_SOUND_MODE = 65536

# Service call validation schemas
MEDIA_PLAYER_SCHEMA = vol.Schema({
Expand All @@ -130,6 +133,10 @@
vol.Required(ATTR_INPUT_SOURCE): cv.string,
})

MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_SOUND_MODE): cv.string,
})

MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string,
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
Expand Down Expand Up @@ -165,6 +172,9 @@
SERVICE_SELECT_SOURCE: {
'method': 'async_select_source',
'schema': MEDIA_PLAYER_SELECT_SOURCE_SCHEMA},
SERVICE_SELECT_SOUND_MODE: {
'method': 'async_select_sound_mode',
'schema': MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA},
SERVICE_PLAY_MEDIA: {
'method': 'async_play_media',
'schema': MEDIA_PLAYER_PLAY_MEDIA_SCHEMA},
Expand Down Expand Up @@ -195,6 +205,8 @@
ATTR_APP_NAME,
ATTR_INPUT_SOURCE,
ATTR_INPUT_SOURCE_LIST,
ATTR_SOUND_MODE,
ATTR_SOUND_MODE_LIST,
ATTR_MEDIA_SHUFFLE,
]

Expand Down Expand Up @@ -344,6 +356,17 @@ def select_source(hass, source, entity_id=None):
hass.services.call(DOMAIN, SERVICE_SELECT_SOURCE, data)


@bind_hass
def select_sound_mode(hass, sound_mode, entity_id=None):
"""Send the media player the command to select sound mode."""
data = {ATTR_SOUND_MODE: sound_mode}

if entity_id:
data[ATTR_ENTITY_ID] = entity_id

hass.services.call(DOMAIN, SERVICE_SELECT_SOUND_MODE, data)


@bind_hass
def clear_playlist(hass, entity_id=None):
"""Send the media player the command for clear playlist."""
Expand All @@ -368,14 +391,10 @@ def async_setup(hass, config):
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)

hass.http.register_view(MediaPlayerImageView(component.entities))
hass.http.register_view(MediaPlayerImageView(component))

yield from component.async_setup(config)

descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))

@asyncio.coroutine
def async_service_handler(service):
"""Map services to methods on MediaPlayerDevice."""
Expand All @@ -392,6 +411,8 @@ def async_service_handler(service):
params['position'] = service.data.get(ATTR_MEDIA_SEEK_POSITION)
elif service.service == SERVICE_SELECT_SOURCE:
params['source'] = service.data.get(ATTR_INPUT_SOURCE)
elif service.service == SERVICE_SELECT_SOUND_MODE:
params['sound_mode'] = service.data.get(ATTR_SOUND_MODE)
elif service.service == SERVICE_PLAY_MEDIA:
params['media_type'] = \
service.data.get(ATTR_MEDIA_CONTENT_TYPE)
Expand All @@ -418,7 +439,7 @@ def async_service_handler(service):
'schema', MEDIA_PLAYER_SCHEMA)
hass.services.async_register(
DOMAIN, service, async_service_handler,
descriptions.get(service), schema=schema)
schema=schema)

return True

Expand Down Expand Up @@ -574,6 +595,16 @@ def source_list(self):
"""List of available input sources."""
return None

@property
def sound_mode(self):
"""Name of the current sound mode."""
return None

@property
def sound_mode_list(self):
"""List of available sound modes."""
return None

@property
def shuffle(self):
"""Boolean if shuffle is enabled."""
Expand Down Expand Up @@ -717,6 +748,17 @@ def async_select_source(self, source):
"""
return self.hass.async_add_job(self.select_source, source)

def select_sound_mode(self, sound_mode):
"""Select sound mode."""
raise NotImplementedError()

def async_select_sound_mode(self, sound_mode):
"""Select sound mode.

This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.select_sound_mode, sound_mode)

def clear_playlist(self):
"""Clear players playlist."""
raise NotImplementedError()
Expand Down Expand Up @@ -790,6 +832,11 @@ def support_select_source(self):
"""Boolean if select source command supported."""
return bool(self.supported_features & SUPPORT_SELECT_SOURCE)

@property
def support_select_sound_mode(self):
"""Boolean if select sound mode command supported."""
return bool(self.supported_features & SUPPORT_SELECT_SOUND_MODE)

@property
def support_clear_playlist(self):
"""Boolean if clear playlist command supported."""
Expand Down Expand Up @@ -935,14 +982,14 @@ class MediaPlayerImageView(HomeAssistantView):
url = '/api/media_player_proxy/{entity_id}'
name = 'api:media_player:image'

def __init__(self, entities):
def __init__(self, component):
"""Initialize a media player view."""
self.entities = entities
self.component = component

@asyncio.coroutine
def get(self, request, entity_id):
"""Start a get request."""
player = self.entities.get(entity_id)
player = self.component.get_entity(entity_id)
if player is None:
status = 404 if request[KEY_AUTHENTICATED] else 401
return web.Response(status=status)
Expand Down
84 changes: 76 additions & 8 deletions homeassistant/components/media_player/denonavr.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
"""

import logging
from collections import namedtuple
from collections import (namedtuple, OrderedDict)
import urllib
import xml.etree.ElementTree as ET
import voluptuous as vol

from homeassistant.components.media_player import (
SUPPORT_PAUSE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL,
MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_TURN_ON,
MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET, SUPPORT_PLAY)
SUPPORT_SELECT_SOURCE, SUPPORT_SELECT_SOUND_MODE,
SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL, MediaPlayerDevice,
PLATFORM_SCHEMA, SUPPORT_TURN_ON, MEDIA_TYPE_MUSIC,
SUPPORT_VOLUME_SET, SUPPORT_PLAY)
from homeassistant.const import (
CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT)
Expand All @@ -27,8 +30,19 @@
DEFAULT_NAME = None
DEFAULT_SHOW_SOURCES = False
DEFAULT_TIMEOUT = 2
DEFAULT_SOUND_MODE = True
DEFAULT_SOUND_MODE_DICT = OrderedDict([('MUSIC', 'PLII MUSIC'),
('MOVIE', 'PLII MOVIE'),
('GAME', 'PLII GAME'),
('PURE DIRECT', 'DIRECT'),
('AUTO', 'None'),
('DOLBY DIGITAL', 'DOLBY DIGITAL'),
('MCH STEREO', 'MULTI CH STEREO'),
('STEREO', 'STEREO')])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something that needs to be configurable / statically defined, or could it be fetched from the device?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, for now it needs to be statically defined because I could not figure out how to get that data from the receiver. At least it is not in the XML file that you can request from the receiver.
However in the official phone aplication of Marantz you can also change the sound mode and their is a list of sound modes their and it nows wich sound mode is currently set. Since this phone application is generic for all Marantz Receivers (that support ethernet), their must be a way to get this information. I am pretty sure the phone application uses normal http requests just like the build in web server does.

Theirfor it is probably possible, I just do not know how.
And at the moment I do not have enough time to figure this out.

It was already fairly difficult to get the whole dictionary working as a configurable option, because the voluptuous checker was not to happy with the syntax.
So for now I think we have to settle for this, but in the future it would definetly be a greath improvement to get this working using some sort of request to the receiver.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think it is fine (for now) ho hardcode that, although I think that the proper place for this is also inside denonavr library rather than here.

I understand that it can be quite hard to decipher what is going on, but assuming you want to look into it you may want to run tcpdump on your router to capture the traffic for later analysis with wireshark.

CONF_SHOW_ALL_SOURCES = 'show_all_sources'
CONF_ZONES = 'zones'
CONF_SOUND_MODE = 'sound_mode'
CONF_SOUND_MODE_DICT = 'sound_mode_dict'
CONF_VALID_ZONES = ['Zone2', 'Zone3']
CONF_INVALID_ZONES_ERR = 'Invalid Zone (expected Zone2 or Zone3)'
KEY_DENON_CACHE = 'denonavr_hosts'
Expand All @@ -49,6 +63,9 @@
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SOUND_MODE, default=DEFAULT_SOUND_MODE): cv.boolean,
vol.Optional(CONF_SOUND_MODE_DICT,
default=DEFAULT_SOUND_MODE_DICT): vol.Schema({str: str}),
vol.Optional(CONF_SHOW_ALL_SOURCES, default=DEFAULT_SHOW_SOURCES):
cv.boolean,
vol.Optional(CONF_ZONES):
Expand All @@ -74,6 +91,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
# Get config option for show_all_sources and timeout
show_all_sources = config.get(CONF_SHOW_ALL_SOURCES)
timeout = config.get(CONF_TIMEOUT)
sound_mode_support = config.get(CONF_SOUND_MODE)
sound_mode_dict = config.get(CONF_SOUND_MODE_DICT)

# Get config option for additional zones
zones = config.get(CONF_ZONES)
Expand Down Expand Up @@ -117,7 +136,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
show_all_inputs=show_all_sources, timeout=timeout,
add_zones=add_zones)
for new_zone in new_device.zones.values():
receivers.append(DenonDevice(new_zone))
receivers.append(DenonDevice(new_zone, host,
sound_mode_support,
sound_mode_dict))
cache.add(host)
_LOGGER.info("Denon receiver at host %s initialized", host)

Expand All @@ -129,14 +150,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DenonDevice(MediaPlayerDevice):
"""Representation of a Denon Media Player Device."""

def __init__(self, receiver):
def __init__(self, receiver, host, sound_mode_support, sound_mode_dict):
"""Initialize the device."""
self._receiver = receiver
self._name = self._receiver.name
self._host = host
self._muted = self._receiver.muted
self._volume = self._receiver.volume
self._current_source = self._receiver.input_func
self._source_list = self._receiver.input_func_list
self._current_sound_mode = None
self._sound_mode_list = list(sound_mode_dict)
self._sound_mode_dict = sound_mode_dict
self._sound_mode_support = sound_mode_support
self._state = self._receiver.state
self._power = self._receiver.power
self._media_image_url = self._receiver.image_url
Expand All @@ -147,6 +173,10 @@ def __init__(self, receiver):
self._frequency = self._receiver.frequency
self._station = self._receiver.station

self._supported_features_base = SUPPORT_DENON
self._supported_features_base |= (sound_mode_support and
SUPPORT_SELECT_SOUND_MODE)

def update(self):
"""Get the latest status information from device."""
self._receiver.update()
Expand All @@ -165,6 +195,26 @@ def update(self):
self._frequency = self._receiver.frequency
self._station = self._receiver.station

if self._sound_mode_support:
try:
url = ('http://' + str(self._host) +
'/goform/formMainZone_MainZoneXml.xml')
xml_data = urllib.request.urlopen(url)
except urllib.error.URLError:
err = "Denon receiver failed to get sound mode, URL-Error"
_LOGGER.error(err)
return
parsed_data = ET.parse(xml_data).getroot()
sound_mode_raw = parsed_data.find('selectSurround/value')
sound_mode_raw = sound_mode_raw.text.rstrip()
try:
mode_list = list(self._sound_mode_dict.values())
mode_index = mode_list.index(sound_mode_raw.upper())
sound_mode = list(self._sound_mode_dict.keys())[mode_index]
self._current_sound_mode = sound_mode
except ValueError:
self._current_sound_mode = sound_mode_raw
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO protocol parsing should be done inside denonavr library and not here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The denonavr library does not support sound mode. I have looked at that before I started doing this code myself in Home Assistant.
I do agree that it would be nicer to have this implemented in the denonavr library.
However I do not know the denonavr library at all and do not know any of the developers.
I looked at their code, but it is not as straightforward because they cover all kinds of exceptions and broke up the simple http requests in their big architecture.
Theirfore I chose to just implement it in HASS.

If anyone has the time and knowledge to implement this in denonavr library please do, you can use my code and I am willing to help. But it looked to complicated for me right now and I do not have the time at the moment.

However this code does work and I have tested it.

I propose we go with this for now and maybe someone has time to later implement this further in the denonavr library to get it a little neater.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should try to get this merged to denonavr, that's the normal procedure for homeassistant libs anyway, and I think no HA developer is very eager to merge such directly. Maybe you could ask if the authors of that lib are willing to help you to get it included? I think that would be the best way to handle this.


@property
def name(self):
"""Return the name of the device."""
Expand Down Expand Up @@ -197,12 +247,22 @@ def source_list(self):
"""Return a list of available input sources."""
return self._source_list

@property
def sound_mode(self):
"""Return the current sound mode."""
return self._current_sound_mode

@property
def sound_mode_list(self):
"""Return a list of available sound modes."""
return self._sound_mode_list

@property
def supported_features(self):
"""Flag media player features that are supported."""
if self._current_source in self._receiver.netaudio_func_list:
return SUPPORT_DENON | SUPPORT_MEDIA_MODES
return SUPPORT_DENON
return self._supported_features_base | SUPPORT_MEDIA_MODES
return self._supported_features_base

@property
def media_content_id(self):
Expand Down Expand Up @@ -292,6 +352,14 @@ def select_source(self, source):
"""Select input source."""
return self._receiver.set_input_func(source)

def select_sound_mode(self, sound_mode):
"""Select sound mode."""
url = ('http://' + str(self._host) +
'/MainZone/index.put.asp?cmd0=PutSurroundMode%2F'
+ sound_mode.upper().replace(" ", "+"))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above wrt denonavr lib.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above

urllib.request.urlopen(url)
return sound_mode

def turn_on(self):
"""Turn on media player."""
if self._receiver.power_on():
Expand Down
Loading