Denos HEOS support

I’m not the greatest Python wizard but I forked the original and added user_login/play_favourite support (because that’s personally what I missed).

It lacks a bit of documentation at the moment :wink:

Seems nice! What’s the purpose of the user_login feature?

Did you use aioheos from Jarle Hjortland? (https://github.com/jarlebh/aioheos/commit/484c3a791f9d80d1d095a320fa610c29d533f65e) it has many features added like the input and sound mode selection etc.

This custom component doesn’t work anymore with version 0.88 :disappointed_relieved:

This is the error I get:
ImportError: cannot import name 'MEDIA_TYPE_MUSIC' from 'homeassistant.components.media_player' (/usr/local/lib/python3.7/site-packages/homeassistant/components/media_player/__init__.py)

Same here, to bad it’s the component I use the most, looks like time to revert back

Looks like this is very similar to the problem someone had here: Native support for Android TV / Android devices

Maybe someone can tweak the top of our py script in a similar fashion, i tried this, but getting a different error now:

from homeassistant.components.media_player import ( MediaPlayerDevice, PLATFORM_SCHEMA, CONF_HOST, CONF_NAME,)

from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC,
    SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, # SUPPORT_VOLUME_STEP,
    SUPPORT_STOP, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
    SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_SEEK,
    SUPPORT_PLAY, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF, MediaPlayerDevice)

No one solved it yet? :weary:

Lampy09 any chance you can maybe update your code to support new media declarations? Also how do you play favorites?

I currently have an automation like this: https://pastebin.com/2kfcm2tu
Where “s75171” is the ID of a radio station in my favorites and “media_content_type: CHANNEL” basically means play favorite.

I’ll have a look next week if I can fix the issues related to the update and if I can make something to provide the available ID’s to the user interface.

I want to start by saying, by no means am I a programmer but fiddled my way through getting this to working in 0.88. Please feel free to correct the errors in my ways.

I made this change to the heos.py file:

From this

from homeassistant.components.media_player import ( # pylint: disable=no-name-in-module
    PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC,
    SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, # SUPPORT_VOLUME_STEP,
    SUPPORT_STOP, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
    SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_SEEK,
    SUPPORT_PLAY, MediaPlayerDevice)

To this:

from homeassistant.components.media_player import (
    PLATFORM_SCHEMA, MediaPlayerDevice)
from homeassistant.components.media_player.const import ( # pylint: disable=no-name-in-module
    MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_MUTE, 
	SUPPORT_VOLUME_SET, # SUPPORT_VOLUME_STEP,
    SUPPORT_STOP, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
    SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_SEEK,
    SUPPORT_PLAY)

I also had to copy Lampy’s aioheos.py and aioheosupnp.py to the \config\deps\lib\python3.7\site-packages\aioheos folder.

Seems to be working now.

Woohoo!! Nice, it’s funny I just had someone else help me with it as well! I got so excited and came here to share the solution and you beat me to it :joy:

here is a fix so graciously provided by @Onandon, in case someone wants to try that… (you don’t have to do any file copying like in your example).

… starting from Line 9 in heos.py

from homeassistant.components.media_player import ( # pylint: disable=no-name-in-module
PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, # SUPPORT_VOLUME_STEP,
SUPPORT_STOP, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_SEEK,
SUPPORT_PLAY, MediaPlayerDevice)

from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF)

import homeassistant.helpers.config_validation as cv

Love this! How do you find this favorite ID though? Is it displayed somewhere?

This does not work for me in 0.88. I get the same error as Cadish.

Here is a link to my heos.py that works for me - http://secure.fabriceleven.com/X7lWr3

Nice one, @skynet01! I’ve merged your code with @Lampy09’s, so it works with authentication & play of favourites too…

Seems to be working for me so far…

"""
Denon Heos notification service.
"""
import asyncio

import logging
import voluptuous as vol

from homeassistant.components.media_player.const import ( # pylint: disable=no-name-in-module
MEDIA_TYPE_MUSIC,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP,
SUPPORT_STOP, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_SEEK,
SUPPORT_PLAY)
from homeassistant.components.media_player import (MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF)
import homeassistant.helpers.config_validation as cv


REQUIREMENTS = ['https://github.com/Lampy09/aioheos/archive/v0.1.5.zip#aioheos==0.1.5']

DEFAULT_NAME = 'HEOS Player'

SUPPORT_HEOS = SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_PAUSE | SUPPORT_PLAY_MEDIA | \
        SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
        SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | SUPPORT_SEEK

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Optional(CONF_HOST): cv.string,
    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
    vol.Optional(CONF_USERNAME): cv.string,
    vol.Optional(CONF_PASSWORD): cv.string,
})

_LOGGER = logging.getLogger(__name__)

# from aioheos import AioHeos, AioHeosException
#from aioheos import AioHeos # pylint: disable=wrong-import-position

@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discover_info=None):
    # pylint: disable=unused-argument
    """Setup the HEOS platform."""

    host = config.get(CONF_HOST)
    name = config.get(CONF_NAME)
    username = config.get(CONF_USERNAME)
    password = config.get(CONF_PASSWORD)

    hass.loop.set_debug(False)
    heos = HeosMediaPlayer(hass, host, name, username, password)

    yield from heos.heos.connect(
        host=host,
        callback=heos.async_update_ha_state
        )

    async_add_devices([heos])


class HeosMediaPlayer(MediaPlayerDevice):
    """ The media player ."""
    # pylint: disable=abstract-method
    # pylint: disable=too-many-public-methods
    # pylint: disable=too-many-instance-attributes

    def __init__(self, hass, host, name, username, password):
        """Initialize"""
        from aioheos import AioHeos
        if host is None:
            _LOGGER.info('No host provided, will try to discover...')
        self._hass = hass
        self.heos = AioHeos(loop=hass.loop, host=host, username=username, password=password, verbose=True)
        self._name = name
        self._state = None

    @asyncio.coroutine
    def async_update(self):
        """Retrieve latest state."""
        self.heos.request_play_state()
        self.heos.request_mute_state()
        self.heos.request_volume()
        self.heos.request_now_playing_media()
        return True

    @property
    def name(self):
        """Return the name of the device."""
        return self._name

    @property
    def volume_level(self):
        """Volume level of the device (0..1)."""
        volume = self.heos.get_volume()
        return float(volume) / 100.0

    @property
    def state(self):
        self._state = self.heos.get_play_state()
        if self._state == 'stop':
            return STATE_OFF
        elif self._state == 'pause':
            return STATE_PAUSED
        elif self._state == 'play':
            return STATE_PLAYING
        else:
            return STATE_UNKNOWN

    @property
    def should_poll(self):
        """No polling needed."""
        return False

    @property
    def media_content_type(self):
        """Content type of current playing media."""
        return MEDIA_TYPE_MUSIC

    @property
    def media_artist(self):
        """Artist of current playing media."""
        return self.heos.get_media_artist()

    @property
    def media_title(self):
        """Album name of current playing media."""
        return self.heos.get_media_song()

    @property
    def media_album_name(self):
        """Album name of current playing media."""
        return self.heos.get_media_album()

    @property
    def media_image_url(self):
        """Return the image url of current playing media."""
        return self.heos.get_media_image_url()

    @property
    def media_content_id(self):
        """Return the content ID of current playing media."""
        return self.heos.get_media_id()

    @property
    def is_volume_muted(self):
        """Boolean if volume is currently muted."""
        muted_state = self.heos.get_mute_state()
        return muted_state == 'on'

    @asyncio.coroutine
    def async_mute_volume(self, mute): # pylint: disable=unused-argument
        """Mute volume"""
        self.heos.toggle_mute()

    @property
    def media_duration(self):
        """Duration of current playing media in seconds."""
        return self.heos.get_duration()/1000.0

    @property
    def media_position_updated_at(self):
        return self.heos.get_position_updated_at()

    @property
    def media_position(self):
        return self.heos.get_position()/1000.0

    @asyncio.coroutine
    def async_media_next_track(self):
        """Go TO next track."""
        self.heos.request_play_next()

    @asyncio.coroutine
    def async_media_previous_track(self):
        """Go TO previous track."""
        self.heos.request_play_previous()

    @asyncio.coroutine
    def async_media_seek(self, position):
        # pylint: disable=no-self-use
        """Seek to posistion."""
        print('MEDIA SEEK', position)

    @property
    def supported_features(self):
        """Flag of media commands that are supported."""
        return SUPPORT_HEOS

    @asyncio.coroutine
    def async_set_volume_level(self, volume):
        """Set volume level, range 0..1."""
        self.heos.set_volume(volume * 100)

    @asyncio.coroutine
    def async_media_play(self):
        """Play media player."""
        self.heos.play()

    @asyncio.coroutine
    def async_media_stop(self):
        """Stop media player."""
        self.heos.stop()

    @asyncio.coroutine
    def async_media_pause(self):
        """Pause media player."""
        self.heos.pause()

    @asyncio.coroutine
    def async_media_play_pause(self):
        """Play or pause the media player."""
        if self._state == 'play':
            yield from self.async_media_pause()
        else:
            yield from self.async_media_play()

    @asyncio.coroutine
    def async_play_media(self, media_type, media_id, **kwargs):
        if media_type == 'CHANNEL':
            self.heos.play_favourite(media_id)

Really would like to be able to integrate Jarlebh’s code (https://github.com/jarlebh/aioheos/tree/484c3a791f9d80d1d095a320fa610c29d533f65e), as there seems to be support for groups, which I really miss now… But I’m really not skilled to do this :cry:

Hope someone else can get this done…

1 Like

Yeah i hear you, i think you can also select source and sound modes as well. Maybe it will even fix all the warnings i get in the log.

Thanks, that makes sense now. This is different then what was posted earlier. That’s where I was confused.

It has been a while since I’ve messed with python but I’ll try and have a look and see if I can help integrating Jarlebh’s code.

Also can you post the warnings you are getting? I am not getting any in the log.

My error log is peppered with these warnings when it’s running

Executing <Task finished coro=<_handle_async_response() done, defined at /usr/local/lib/python3.7/site-packages/homeassistant/components/websocket_api/decorators.py:14> result=None created at /usr/local/lib/python3.7/site-packages/homeassistant/core.py:291> took 0.270 seconds

and
Executing <TimerHandle _async_create_timer.<locals>.fire_time_event created at /usr/local/lib/python3.7/site-packages/homeassistant/core.py:1260> took 0.124 seconds