From bd475f5db1b665452fd65e946b2cf296d8f6df79 Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Sun, 31 Jan 2016 10:06:39 +0100 Subject: [PATCH 1/4] Added a new media_player platform for controlling Samsung TVs with a lan interface. Configured like this media_player: platform: samsungtv host: name: --- .../components/media_player/samsungtv.py | 175 ++++++++++++++++++ requirements_all.txt | 3 + 2 files changed, 178 insertions(+) create mode 100644 homeassistant/components/media_player/samsungtv.py diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py new file mode 100644 index 00000000000..7cf23a84d3d --- /dev/null +++ b/homeassistant/components/media_player/samsungtv.py @@ -0,0 +1,175 @@ +""" +homeassistant.components.media_player.denon +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides an interface to Samsung TV with a Laninterface. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.samsungtv/ +""" +import logging +import socket +from samsungctl import Remote + +from homeassistant.components.media_player import ( + MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_VOLUME_STEP, + SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_NEXT_TRACK, SUPPORT_TURN_OFF, + DOMAIN) +from homeassistant.const import ( + CONF_HOST, STATE_OFF, + STATE_ON, STATE_UNKNOWN) + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_SAMSUNGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ + SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_NEXT_TRACK | 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 + name = config.get("name", 'Samsung TV Remote') + # Generate a config for the Samsung lib + remote_config = { + "name": "HomeAssistant", + "description": config.get("name", ''), + "id": "ha.component.samsung", + "port": config.get("port", 55000), + "host": config.get("host"), + "timeout": config.get("timeout", 0), + } + + add_devices([SamsungTVDevice(name, remote_config)]) + + +# pylint: disable=too-many-public-methods +class SamsungTVDevice(MediaPlayerDevice): + """ Represents a Denon device. """ + + def set_volume_level(self, volume): + pass + + # pylint: disable=too-many-public-methods + + def __init__(self, name, config): + self._name = name + # Assume that the TV is not muted + self._muted = False + # Assume that the TV is in Play mode + self._playing = True + self._state = STATE_UNKNOWN + self._remote = None + self._config = config + + def update(self): + # Send an empty key to see if we are still connected + return self.send_key('KEY_POWER') + + def get_remote(self): + """ Creates or Returns a remote control instance """ + if self._remote is None: + # We need to create a new instance to reconnect. + self._remote = Remote(self._config) + + return self._remote + + def send_key(self, key): + """ Sends a key to the tv and handles exceptions """ + try: + self.get_remote().control(key) + self._state = STATE_ON + except (Remote.UnhandledResponse, Remote.AccessDenied, + BrokenPipeError): + # We got a response so it's on. + # BrokenPipe can occur when the commands is sent to fast + self._state = STATE_ON + self._remote = None + return False + except (Remote.ConnectionClosed, Remote.ConnectionClosed, + socket.timeout, TimeoutError, OSError): + self._state = STATE_OFF + self._remote = None + return False + except Remote.AccessDenied: + self._state = STATE_ON + return False + + return True + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + return self._state + + @property + def is_volume_muted(self): + """ Boolean if volume is currently muted. """ + return self._muted + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return SUPPORT_SAMSUNGTV + + def turn_off(self): + """ turn_off media player. """ + self.send_key("KEY_POWEROFF") + + def volume_up(self): + """ volume_up media player. """ + self.send_key("KEY_VOLUP") + + def volume_down(self): + """ volume_down media player. """ + self.send_key("KEY_VOLDOWN") + + def mute_volume(self, mute): + self.send_key("KEY_MUTE") + + def media_play_pause(self): + """ Simulate play pause media player. """ + if self._playing: + self.media_pause() + else: + self.media_play() + + def media_play(self): + """ media_play media player. """ + self._playing = True + self.send_key("KEY_PLAY") + + def media_pause(self): + """ media_pause media player. """ + self._playing = False + self.send_key("KEY_PAUSE") + + def media_next_track(self): + """ Send next track command. """ + self.send_key("KEY_FF") + + def media_previous_track(self): + self.send_key("KEY_REWIND") + + def media_seek(self, position): + raise NotImplementedError() + + def turn_on(self): + """ turn the media player on. """ + self.send_key("KEY_POWERON") + + def play_media(self, media_type, media_id): + pass + + def play_youtube(self, media_id): + pass diff --git a/requirements_all.txt b/requirements_all.txt index 80369444496..5dd8078e46e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -95,6 +95,9 @@ plexapi==1.1.0 # homeassistant.components.media_player.sonos SoCo==0.11.1 +# homeassistant.components.media_player.samsungtv +samsungctl==0.5.1 + # homeassistant.components.modbus https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0 From 2dab815f90d96e833378bad5278718e12cc19d46 Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Sun, 31 Jan 2016 19:12:00 +0100 Subject: [PATCH 2/4] Fixes imports, styles and other misstates --- .../components/media_player/samsungtv.py | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 7cf23a84d3d..74ba5b5e526 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -8,7 +8,6 @@ https://home-assistant.io/components/media_player.samsungtv/ """ import logging import socket -from samsungctl import Remote from homeassistant.components.media_player import ( MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_VOLUME_STEP, @@ -16,11 +15,16 @@ from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_TURN_OFF, DOMAIN) from homeassistant.const import ( - CONF_HOST, STATE_OFF, + CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) +CONF_PORT = "port" +CONF_TIMEOUT = "timeout" + _LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['samsungctl==0.5.1'] + SUPPORT_SAMSUNGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF @@ -28,7 +32,8 @@ SUPPORT_SAMSUNGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the Denon platform. """ + """ Sets up the Samsung TV platform. """ + if not config.get(CONF_HOST): _LOGGER.error( "Missing required configuration items in %s: %s", @@ -39,26 +44,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # Generate a config for the Samsung lib remote_config = { "name": "HomeAssistant", - "description": config.get("name", ''), + "description": config.get(CONF_NAME, ''), "id": "ha.component.samsung", - "port": config.get("port", 55000), - "host": config.get("host"), - "timeout": config.get("timeout", 0), + "port": config.get(CONF_PORT, 55000), + "host": config.get(CONF_HOST), + "timeout": config.get(CONF_TIMEOUT, 0), } add_devices([SamsungTVDevice(name, remote_config)]) -# pylint: disable=too-many-public-methods +# pylint: disable=abstract-method class SamsungTVDevice(MediaPlayerDevice): - """ Represents a Denon device. """ - - def set_volume_level(self, volume): - pass + """ Represents a Samsung TV. """ # pylint: disable=too-many-public-methods - def __init__(self, name, config): + self._name = name # Assume that the TV is not muted self._muted = False @@ -74,6 +76,8 @@ class SamsungTVDevice(MediaPlayerDevice): def get_remote(self): """ Creates or Returns a remote control instance """ + from samsungctl import Remote + if self._remote is None: # We need to create a new instance to reconnect. self._remote = Remote(self._config) @@ -82,6 +86,7 @@ class SamsungTVDevice(MediaPlayerDevice): def send_key(self, key): """ Sends a key to the tv and handles exceptions """ + from samsungctl import Remote try: self.get_remote().control(key) self._state = STATE_ON @@ -92,14 +97,11 @@ class SamsungTVDevice(MediaPlayerDevice): self._state = STATE_ON self._remote = None return False - except (Remote.ConnectionClosed, Remote.ConnectionClosed, - socket.timeout, TimeoutError, OSError): + except (Remote.ConnectionClosed, socket.timeout, + TimeoutError, OSError): self._state = STATE_OFF self._remote = None return False - except Remote.AccessDenied: - self._state = STATE_ON - return False return True @@ -161,15 +163,6 @@ class SamsungTVDevice(MediaPlayerDevice): def media_previous_track(self): self.send_key("KEY_REWIND") - def media_seek(self, position): - raise NotImplementedError() - def turn_on(self): """ turn the media player on. """ self.send_key("KEY_POWERON") - - def play_media(self, media_type, media_id): - pass - - def play_youtube(self, media_id): - pass From 5719743ec7a14ef016306d8e69f87666c5d280be Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Sun, 31 Jan 2016 20:02:51 +0100 Subject: [PATCH 3/4] Fixed .coveragerc and requirements_all.txt --- .coveragerc | 1 + requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index 75f4cbd20fd..11e4edad8c1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -91,6 +91,7 @@ omit = homeassistant/components/media_player/plex.py homeassistant/components/media_player/sonos.py homeassistant/components/media_player/squeezebox.py + homeassistant/components/media_player/samsungtv.py homeassistant/components/notify/free_mobile.py homeassistant/components/notify/instapush.py homeassistant/components/notify/nma.py diff --git a/requirements_all.txt b/requirements_all.txt index 5dd8078e46e..29379bd52c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -92,12 +92,12 @@ python-mpd2==0.5.4 # homeassistant.components.media_player.plex plexapi==1.1.0 -# homeassistant.components.media_player.sonos -SoCo==0.11.1 - # homeassistant.components.media_player.samsungtv samsungctl==0.5.1 +# homeassistant.components.media_player.sonos +SoCo==0.11.1 + # homeassistant.components.modbus https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0 From 1c10f218debe64f7ece4dbf855c66e0829c76174 Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Sun, 31 Jan 2016 22:17:00 +0100 Subject: [PATCH 4/4] Fixed duplicate import statements and made use of the config_helper --- .../components/media_player/samsungtv.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 74ba5b5e526..1d2703b0e2e 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -18,6 +18,8 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) +from homeassistant.helpers import validate_config + CONF_PORT = "port" CONF_TIMEOUT = "timeout" @@ -34,13 +36,13 @@ SUPPORT_SAMSUNGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Samsung TV platform. """ - if not config.get(CONF_HOST): - _LOGGER.error( - "Missing required configuration items in %s: %s", - DOMAIN, - CONF_HOST) + # Validate that all required config options are given + if not validate_config({DOMAIN: config}, {DOMAIN: [CONF_HOST]}, _LOGGER): return False - name = config.get("name", 'Samsung TV Remote') + + # Default the entity_name to 'Samsung TV Remote' + name = config.get(CONF_NAME, 'Samsung TV Remote') + # Generate a config for the Samsung lib remote_config = { "name": "HomeAssistant", @@ -60,7 +62,9 @@ class SamsungTVDevice(MediaPlayerDevice): # pylint: disable=too-many-public-methods def __init__(self, name, config): - + from samsungctl import Remote + # Save a reference to the imported class + self._remote_class = Remote self._name = name # Assume that the TV is not muted self._muted = False @@ -76,28 +80,26 @@ class SamsungTVDevice(MediaPlayerDevice): def get_remote(self): """ Creates or Returns a remote control instance """ - from samsungctl import Remote if self._remote is None: # We need to create a new instance to reconnect. - self._remote = Remote(self._config) + self._remote = self._remote_class(self._config) return self._remote def send_key(self, key): """ Sends a key to the tv and handles exceptions """ - from samsungctl import Remote try: self.get_remote().control(key) self._state = STATE_ON - except (Remote.UnhandledResponse, Remote.AccessDenied, - BrokenPipeError): + except (self._remote_class.UnhandledResponse, + self._remote_class.AccessDenied, BrokenPipeError): # We got a response so it's on. # BrokenPipe can occur when the commands is sent to fast self._state = STATE_ON self._remote = None return False - except (Remote.ConnectionClosed, socket.timeout, + except (self._remote_class.ConnectionClosed, socket.timeout, TimeoutError, OSError): self._state = STATE_OFF self._remote = None