diff --git a/.coveragerc b/.coveragerc index 5e3dc0978ee..1c7b05c7d24 100644 --- a/.coveragerc +++ b/.coveragerc @@ -68,6 +68,10 @@ omit = homeassistant/components/upnp.py homeassistant/components/zeroconf.py + homeassistant/components/*/thinkingcleaner.py + + homeassistant/components/*/webostv.py + homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/rest.py homeassistant/components/browser.py @@ -179,7 +183,6 @@ omit = homeassistant/components/thermostat/homematic.py homeassistant/components/thermostat/proliphix.py homeassistant/components/thermostat/radiotherm.py - homeassistant/components/*/thinkingcleaner.py [report] diff --git a/homeassistant/components/frontend/www_static/images/config_webos.png b/homeassistant/components/frontend/www_static/images/config_webos.png new file mode 100644 index 00000000000..757aec76270 Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/config_webos.png differ diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py new file mode 100644 index 00000000000..2e22e7daf31 --- /dev/null +++ b/homeassistant/components/media_player/webostv.py @@ -0,0 +1,249 @@ +""" +Support for interface with an LG WebOS TV. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.webostv/ +""" +import logging +from datetime import timedelta +from urllib.parse import urlparse + +import homeassistant.util as util +from homeassistant.components.media_player import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, + SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL, + MediaPlayerDevice) +from homeassistant.const import ( + CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN) +from homeassistant.loader import get_component + +_CONFIGURING = {} +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv' + '/archive/v0.1.1.zip' + '#pylgtv==0.1.1'] + +SUPPORT_WEBOSTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ + SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \ + SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the LG WebOS TV platform.""" + if discovery_info is not None: + host = urlparse(discovery_info[1]).hostname + else: + host = config.get(CONF_HOST, None) + + if host is None: + _LOGGER.error('No host found in configuration') + return False + + # Only act if we are not already configuring this host + if host in _CONFIGURING: + return + + setup_tv(host, hass, add_devices) + + +def setup_tv(host, hass, add_devices): + """Setup a phue bridge based on host parameter.""" + from pylgtv import WebOsClient + from pylgtv import PyLGTVPairException + + client = WebOsClient(host) + + if not client.is_registered(): + if host in _CONFIGURING: + # Try to pair. + try: + client.register() + except PyLGTVPairException: + _LOGGER.warning( + 'Connected to LG WebOS TV at %s but not paired.', host) + return + except ConnectionRefusedError: + _LOGGER.error('Unable to connect to host %s.', host) + return + else: + # Not registered, request configuration. + _LOGGER.warning('LG WebOS TV at %s needs to be paired.', host) + request_configuration(host, hass, add_devices) + return + + # If we came here and configuring this host, mark as done. + if client.is_registered() and host in _CONFIGURING: + request_id = _CONFIGURING.pop(host) + configurator = get_component('configurator') + configurator.request_done(request_id) + + add_devices([LgWebOSDevice(host)]) + + +def request_configuration(host, hass, add_devices): + """Request configuration steps from the user.""" + configurator = get_component('configurator') + + # We got an error if this method is called while we are configuring + if host in _CONFIGURING: + configurator.notify_errors( + _CONFIGURING[host], 'Failed to pair, please try again.') + return + + # pylint: disable=unused-argument + def lgtv_configuration_callback(data): + """The actions to do when our configuration callback is called.""" + setup_tv(host, hass, add_devices) + + _CONFIGURING[host] = configurator.request_config( + hass, 'LG WebOS TV', lgtv_configuration_callback, + description='Click start and accept the pairing request on your tv.', + description_image='/static/images/config_webos.png', + submit_caption='Start pairing request' + ) + + +# pylint: disable=abstract-method +# pylint: disable=too-many-instance-attributes +class LgWebOSDevice(MediaPlayerDevice): + """Representation of a LG WebOS TV.""" + + # pylint: disable=too-many-public-methods + def __init__(self, host): + """Initialize the webos device.""" + from pylgtv import WebOsClient + self._client = WebOsClient(host) + + self._name = 'LG WebOS TV Remote' + # Assume that the TV is not muted + self._muted = False + # Assume that the TV is in Play mode + self._playing = True + self._volume = 0 + self._current_source = None + self._current_source_id = None + self._source_list = None + self._state = STATE_UNKNOWN + self._app_list = None + + self.update() + + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + def update(self): + """Retrieve the latest data.""" + try: + self._state = STATE_PLAYING + self._muted = self._client.get_muted() + self._volume = self._client.get_volume() + self._current_source_id = self._client.get_input() + + self._source_list = [] + self._app_list = {} + for app in self._client.get_apps(): + self._app_list[app['id']] = app + self._source_list.append(app['title']) + if app['id'] == self._current_source_id: + self._current_source = app['title'] + + except ConnectionRefusedError: + self._state = STATE_OFF + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume / 100.0 + + @property + def source(self): + """Return the current input source.""" + return self._current_source + + @property + def source_list(self): + """List of available input sources.""" + return self._source_list + + @property + def media_content_type(self): + """Content type of current playing media.""" + return MEDIA_TYPE_CHANNEL + + @property + def media_image_url(self): + """Image url of current playing media.""" + return self._app_list[self._current_source_id]['icon'] + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_WEBOSTV + + def turn_off(self): + """Turn off media player.""" + self._client.power_off() + + def volume_up(self): + """Volume up the media player.""" + self._client.volume_up() + + def volume_down(self): + """Volume down media player.""" + self._client.volume_down() + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + tv_volume = volume * 100 + self._client.set_volume(tv_volume) + + def mute_volume(self, mute): + """Send mute command.""" + self._client.set_mute(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): + """Send play command.""" + self._playing = True + self._state = STATE_PLAYING + self._client.play() + + def media_pause(self): + """Send media pause command to media player.""" + self._playing = False + self._state = STATE_PAUSED + self._client.pause() + + def media_next_track(self): + """Send next track command.""" + self._client.fast_forward() + + def media_previous_track(self): + """Send the previous track command.""" + self._client.rewind() diff --git a/homeassistant/components/notify/webostv.py b/homeassistant/components/notify/webostv.py new file mode 100644 index 00000000000..fb6cf02c9c5 --- /dev/null +++ b/homeassistant/components/notify/webostv.py @@ -0,0 +1,62 @@ +""" +LG WebOS TV notification service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.webostv/ +""" +import logging + +from homeassistant.components.notify import (BaseNotificationService, DOMAIN) +from homeassistant.const import (CONF_HOST, CONF_NAME) +from homeassistant.helpers import validate_config + +_LOGGER = logging.getLogger(__name__) + + +def get_service(hass, config): + """Return the notify service.""" + if not validate_config({DOMAIN: config}, {DOMAIN: [CONF_HOST, CONF_NAME]}, + _LOGGER): + return None + + host = config.get(CONF_HOST, None) + + if not host: + _LOGGER.error('No host provided.') + return None + + from pylgtv import WebOsClient + from pylgtv import PyLGTVPairException + + client = WebOsClient(host) + + try: + client.register() + except PyLGTVPairException: + _LOGGER.error('Pairing failed.') + return None + except ConnectionRefusedError: + _LOGGER.error('Host unreachable.') + return None + + return LgWebOSNotificationService(client) + + +# pylint: disable=too-few-public-methods +class LgWebOSNotificationService(BaseNotificationService): + """Implement the notification service for LG WebOS TV.""" + + def __init__(self, client): + """Initialize the service.""" + self._client = client + + def send_message(self, message="", **kwargs): + """Send a message to the tv.""" + from pylgtv import PyLGTVPairException + + try: + self._client.send_message(message) + except PyLGTVPairException: + _LOGGER.error('Pairing failed.') + except ConnectionRefusedError: + _LOGGER.error('Host unreachable.') diff --git a/requirements_all.txt b/requirements_all.txt index c3bbb5712b5..7dae3df1b85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -79,6 +79,9 @@ https://github.com/HydrelioxGitHub/netatmo-api-python/archive/43ff238a0122b0939a # homeassistant.components.switch.dlink https://github.com/LinuxChristian/pyW215/archive/v0.1.1.zip#pyW215==0.1.1 +# homeassistant.components.media_player.webostv +https://github.com/TheRealLink/pylgtv/archive/v0.1.1.zip#pylgtv==0.1.1 + # homeassistant.components.sensor.thinkingcleaner # homeassistant.components.switch.thinkingcleaner https://github.com/TheRealLink/pythinkingcleaner/archive/v0.0.2.zip#pythinkingcleaner==0.0.2