From c7a8f5d6ca9f9494c9d4977fe51b7d1c63bc32aa Mon Sep 17 00:00:00 2001 From: MakeMeASandwich Date: Sun, 6 Sep 2015 12:07:12 +0200 Subject: [PATCH 1/2] media_player: add Denon remote support --- .coveragerc | 1 + .../components/media_player/denon.py | 192 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 homeassistant/components/media_player/denon.py diff --git a/.coveragerc b/.coveragerc index 0aa3b54f8d9..c68572f9070 100644 --- a/.coveragerc +++ b/.coveragerc @@ -47,6 +47,7 @@ omit = homeassistant/components/light/hue.py homeassistant/components/light/limitlessled.py homeassistant/components/media_player/cast.py + homeassistant/components/media_player/denon.py homeassistant/components/media_player/kodi.py homeassistant/components/media_player/mpd.py homeassistant/components/media_player/squeezebox.py diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py new file mode 100644 index 00000000000..2a3ee9d5ab9 --- /dev/null +++ b/homeassistant/components/media_player/denon.py @@ -0,0 +1,192 @@ +""" +homeassistant.components.media_player.denon +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides an interface to Denon Network Receivers. +Developed for a Denon DRA-N5, see +http://www.denon.co.uk/chg/product/compactsystems/networkmusicsystems/ceolpiccolo + +A few notes: + - As long as this module is active and connected, the receiver does + not seem to accept additional telnet connections. + + - Be careful with the volume. 50% or even 100% are very loud. + + - To be able to wake up the receiver, activate the "remote" setting + in the receiver's settings. + + - Play and pause are supported, toggling is not possible. + + - Seeking cannot be implemented as the UI sends absolute positions. + Only seeking via simulated button presses is possible. + +Configuration: + +To use your Denon you will need to add something like the following to +your config/configuration.yaml: + +media_player: + platform: denon + name: Music station + host: 192.168.0.123 + +Variables: + +host +*Required +The ip of the player. Example: 192.168.0.123 + +name +*Optional +The name of the device. +""" +import telnetlib +import logging + +from homeassistant.components.media_player import ( + MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + DOMAIN) +from homeassistant.const import ( + CONF_HOST, STATE_OFF, STATE_IDLE, STATE_UNKNOWN) + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Denon platform. """ + if not config.get(CONF_HOST): + _LOGGER.error( + "Missing required configuration items in %s: %s", + DOMAIN, + CONF_HOST) + return False + + add_devices([ + DenonDevice( + config.get('name', 'Music station'), + config.get('host')) + ]) + + return True + + +class DenonDevice(MediaPlayerDevice): + """ Represents a Denon device. """ + + # pylint: disable=too-many-public-methods + + def __init__(self, name, host): + self._name = name + self._host = host + self._telnet = telnetlib.Telnet(self._host) + + def query(self, message): + """ Send request and await response from server """ + try: + # unspecified command, should be ignored + self._telnet.write("?".encode('UTF-8') + b'\r') + except (EOFError, BrokenPipeError, ConnectionResetError): + self._telnet.open(self._host) + + _LOGGER.error(self._telnet.read_very_eager()) # skip + + _LOGGER.error(message) + self._telnet.write(message.encode('ASCII') + b'\r') + # timeout 200ms, defined by protocol + resp = self._telnet.read_until(b'\r', timeout=0.2)\ + .decode('UTF-8').strip() + + if message == "PW?": + # workaround; PW? sends also SISTATUS + _LOGGER.error(self._telnet.read_until(b'\r', timeout=0.2)) + _LOGGER.error(resp) + return resp + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + pwstate = self.query('PW?') + if pwstate == "PWSTANDBY": + return STATE_OFF + if pwstate == "PWON": + return STATE_IDLE + + return STATE_UNKNOWN + + @property + def volume_level(self): + """ Volume level of the media player (0..1). """ + return int(self.query('MV?')[len('MV'):]) / 60 + + @property + def is_volume_muted(self): + """ Boolean if volume is currently muted. """ + return self.query('MU?') == "MUON" + + @property + def media_title(self): + """ Current media source. """ + return self.query('SI?')[len('SI'):] + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return SUPPORT_DENON + + def turn_off(self): + """ turn_off media player. """ + self.query('PWSTANDBY') + + def volume_up(self): + """ volume_up media player. """ + self.query('MVUP') + + def volume_down(self): + """ volume_down media player. """ + self.query('MVDOWN') + + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ + # 60dB max + self.query('MV' + str(round(volume * 60)).zfill(2)) + + def mute_volume(self, mute): + """ mute (true) or unmute (false) media player. """ + self.query('MU' + ('ON' if mute else 'OFF')) + + def media_play_pause(self): + """ media_play_pause media player. """ + raise NotImplementedError() + + def media_play(self): + """ media_play media player. """ + self.query('NS9A') + + def media_pause(self): + """ media_pause media player. """ + self.query('NS9B') + + def media_next_track(self): + """ Send next track command. """ + self.query('NS9D') + + def media_previous_track(self): + self.query('NS9E') + + def media_seek(self, position): + raise NotImplementedError() + + def turn_on(self): + """ turn the media player on. """ + self.query('PWON') From 117a0018a5acfd35eeb008fc95357550de484d96 Mon Sep 17 00:00:00 2001 From: MakeMeASandwich Date: Wed, 9 Sep 2015 19:41:57 +0200 Subject: [PATCH 2/2] media_player: remove debug messages, change IDLE to ON --- homeassistant/components/media_player/denon.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py index 2a3ee9d5ab9..19286906f49 100644 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -48,7 +48,7 @@ from homeassistant.components.media_player import ( SUPPORT_TURN_ON, SUPPORT_TURN_OFF, DOMAIN) from homeassistant.const import ( - CONF_HOST, STATE_OFF, STATE_IDLE, STATE_UNKNOWN) + CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN) _LOGGER = logging.getLogger(__name__) @@ -94,9 +94,8 @@ class DenonDevice(MediaPlayerDevice): except (EOFError, BrokenPipeError, ConnectionResetError): self._telnet.open(self._host) - _LOGGER.error(self._telnet.read_very_eager()) # skip + self._telnet.read_very_eager() # skip what is not requested - _LOGGER.error(message) self._telnet.write(message.encode('ASCII') + b'\r') # timeout 200ms, defined by protocol resp = self._telnet.read_until(b'\r', timeout=0.2)\ @@ -104,8 +103,8 @@ class DenonDevice(MediaPlayerDevice): if message == "PW?": # workaround; PW? sends also SISTATUS - _LOGGER.error(self._telnet.read_until(b'\r', timeout=0.2)) - _LOGGER.error(resp) + self._telnet.read_until(b'\r', timeout=0.2) + return resp @property @@ -120,7 +119,7 @@ class DenonDevice(MediaPlayerDevice): if pwstate == "PWSTANDBY": return STATE_OFF if pwstate == "PWON": - return STATE_IDLE + return STATE_ON return STATE_UNKNOWN