From cbc0833360d596cb2d58710295b4559d9c046e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20P=C3=A1rraga=20Navarro?= Date: Mon, 20 Jun 2016 07:35:26 +0200 Subject: [PATCH] Support for Sony Bravia TV (#2243) * Added Sony Bravia support to HA * Improvements to make it work on my poor raspberry 1 * Just a typo * A few fixes in order to pass pylint * - Remove noqa: was due to the 80 characters max per line restriction - Move communication logic to a separate library at https://github.com/aparraga/braviarc.git - Added dependency and adapt the code according to that * A few improvements * Just a typo in a comment * Rebase from HM/dev * Update requirements by executing the script/gen_requirements_all.py * More isolation level for braviarc lib * Remove unnecessary StringIO usage * Revert submodule polymer commit * Small refactorization and clean up of unused functions * Executed script/gen_requirements_all.py * Added a missing condition to ensure that a map is not null * Fix missing parameter detected by pylint * A few improvements, also added an empty line to avoid the lint error * A typo --- .coveragerc | 1 + .../frontend/www_static/images/smart-tv.png | Bin 0 -> 3250 bytes .../components/media_player/braviatv.py | 371 ++++++++++++++++++ requirements_all.txt | 3 + 4 files changed, 375 insertions(+) create mode 100644 homeassistant/components/frontend/www_static/images/smart-tv.png create mode 100644 homeassistant/components/media_player/braviatv.py diff --git a/.coveragerc b/.coveragerc index 265c653d636..a7464feb571 100644 --- a/.coveragerc +++ b/.coveragerc @@ -123,6 +123,7 @@ omit = homeassistant/components/light/limitlessled.py homeassistant/components/light/osramlightify.py homeassistant/components/lirc.py + homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py homeassistant/components/media_player/denon.py homeassistant/components/media_player/firetv.py diff --git a/homeassistant/components/frontend/www_static/images/smart-tv.png b/homeassistant/components/frontend/www_static/images/smart-tv.png new file mode 100644 index 0000000000000000000000000000000000000000..5ecda68b40290303ef4c360f7e001aa5a1708d16 GIT binary patch literal 3250 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 KLJtFSi~TPZy8vte literal 0 HcmV?d00001 diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py new file mode 100644 index 00000000000..ea316f57425 --- /dev/null +++ b/homeassistant/components/media_player/braviatv.py @@ -0,0 +1,371 @@ +""" +Support for interface with a Sony Bravia TV. + +By Antonio Parraga Navarro + +dedicated to Isabel + +""" +import logging +import os +import json +import re +from homeassistant.loader import get_component +from homeassistant.components.media_player import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, + SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, MediaPlayerDevice) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) + +REQUIREMENTS = [ + 'https://github.com/aparraga/braviarc/archive/0.3.2.zip' + '#braviarc==0.3.2'] + +BRAVIA_CONFIG_FILE = 'bravia.conf' +CLIENTID_PREFIX = 'HomeAssistant' +NICKNAME = 'Home Assistant' + +# Map ip to request id for configuring +_CONFIGURING = {} + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_BRAVIA = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ + SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ + SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE + + +def _get_mac_address(ip_address): + from subprocess import Popen, PIPE + + pid = Popen(["arp", "-n", ip_address], stdout=PIPE) + pid_component = pid.communicate()[0] + mac = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})".encode('UTF-8'), + pid_component).groups()[0] + return mac + + +def _config_from_file(filename, config=None): + """Small configuration file management function.""" + if config: + # We're writing configuration + bravia_config = _config_from_file(filename) + if bravia_config is None: + bravia_config = {} + new_config = bravia_config.copy() + new_config.update(config) + try: + with open(filename, 'w') as fdesc: + fdesc.write(json.dumps(new_config)) + except IOError as error: + _LOGGER.error('Saving config file failed: %s', error) + return False + return True + else: + # We're reading config + if os.path.isfile(filename): + try: + with open(filename, 'r') as fdesc: + return json.loads(fdesc.read()) + except ValueError as error: + return {} + except IOError as error: + _LOGGER.error('Reading config file failed: %s', error) + # This won't work yet + return False + else: + return {} + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Setup the Sony Bravia TV platform.""" + host = config.get(CONF_HOST) + + if host is None: + return # if no host configured, do not continue + + pin = None + bravia_config = _config_from_file(hass.config.path(BRAVIA_CONFIG_FILE)) + while len(bravia_config): + # Setup a configured TV + host_ip, host_config = bravia_config.popitem() + if host_ip == host: + pin = host_config['pin'] + mac = host_config['mac'] + name = config.get(CONF_NAME) + add_devices_callback([BraviaTVDevice(host, mac, name, pin)]) + return + + setup_bravia(config, pin, hass, add_devices_callback) + + +# pylint: disable=too-many-branches +def setup_bravia(config, pin, hass, add_devices_callback): + """Setup a sony bravia based on host parameter.""" + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + if name is None: + name = "Sony Bravia TV" + + if pin is None: + request_configuration(config, hass, add_devices_callback) + return + else: + mac = _get_mac_address(host) + if mac is not None: + mac = mac.decode('utf8') + # If we came here and configuring this host, mark as done + if host in _CONFIGURING: + request_id = _CONFIGURING.pop(host) + configurator = get_component('configurator') + configurator.request_done(request_id) + _LOGGER.info('Discovery configuration done!') + + # Save config + if not _config_from_file( + hass.config.path(BRAVIA_CONFIG_FILE), + {host: {'pin': pin, 'host': host, 'mac': mac}}): + _LOGGER.error('failed to save config file') + + add_devices_callback([BraviaTVDevice(host, mac, name, pin)]) + + +def request_configuration(config, hass, add_devices_callback): + """Request configuration steps from the user.""" + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + if name is None: + name = "Sony Bravia" + + 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 register, please try again.") + return + + def bravia_configuration_callback(data): + """Callback after user enter PIN.""" + from braviarc import braviarc + + pin = data.get('pin') + braviarc = braviarc.BraviaRC(host) + braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) + if braviarc.is_connected(): + setup_bravia(config, pin, hass, add_devices_callback) + else: + request_configuration(config, hass, add_devices_callback) + + _CONFIGURING[host] = configurator.request_config( + hass, name, bravia_configuration_callback, + description='Enter the Pin shown on your Sony Bravia TV.' + + 'If no Pin is shown, enter 0000 to let TV show you a Pin.', + description_image="/static/images/smart-tv.png", + submit_caption="Confirm", + fields=[{'id': 'pin', 'name': 'Enter the pin', 'type': ''}] + ) + + +# pylint: disable=abstract-method, too-many-public-methods, +# pylint: disable=too-many-instance-attributes, too-many-arguments +class BraviaTVDevice(MediaPlayerDevice): + """Representation of a Sony Bravia TV.""" + + def __init__(self, host, mac, name, pin): + """Initialize the sony bravia device.""" + from braviarc import braviarc + + self._pin = pin + self._braviarc = braviarc.BraviaRC(host, mac) + self._name = name + self._state = STATE_OFF + self._muted = False + self._program_name = None + self._channel_name = None + self._channel_number = None + self._source = None + self._source_list = [] + self._original_content_list = [] + self._content_mapping = {} + self._duration = None + self._content_uri = None + self._id = None + self._playing = False + self._start_date_time = None + self._program_media_type = None + self._min_volume = None + self._max_volume = None + self._volume = None + + self._braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) + if self._braviarc.is_connected(): + self.update() + else: + self._state = STATE_OFF + + def update(self): + """Update TV info.""" + if not self._braviarc.is_connected(): + self._braviarc.connect(self._pin, CLIENTID_PREFIX, NICKNAME) + if not self._braviarc.is_connected(): + return + + # Retrieve the latest data. + try: + if self._state == STATE_ON: + # refresh volume info: + self._refresh_volume() + self._refresh_channels() + + playing_info = self._braviarc.get_playing_info() + if playing_info is None or len(playing_info) == 0: + self._state = STATE_OFF + else: + self._state = STATE_ON + self._program_name = playing_info.get('programTitle') + self._channel_name = playing_info.get('title') + self._program_media_type = playing_info.get( + 'programMediaType') + self._channel_number = playing_info.get('dispNum') + self._source = playing_info.get('source') + self._content_uri = playing_info.get('uri') + self._duration = playing_info.get('durationSec') + self._start_date_time = playing_info.get('startDateTime') + + except Exception as exception_instance: # pylint: disable=broad-except + _LOGGER.error(exception_instance) + self._state = STATE_OFF + + def _refresh_volume(self): + """Refresh volume information.""" + volume_info = self._braviarc.get_volume_info() + if volume_info is not None: + self._volume = volume_info.get('volume') + self._min_volume = volume_info.get('minVolume') + self._max_volume = volume_info.get('maxVolume') + self._muted = volume_info.get('mute') + + def _refresh_channels(self): + if len(self._source_list) == 0: + self._content_mapping = self._braviarc. \ + load_source_list() + self._source_list = [] + for key in self._content_mapping: + self._source_list.append(key) + + @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 source(self): + """Return the current input source.""" + return self._source + + @property + def source_list(self): + """List of available input sources.""" + return self._source_list + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + if self._volume is not None: + return self._volume / 100 + else: + return None + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_BRAVIA + + @property + def media_title(self): + """Title of current playing media.""" + return_value = None + if self._channel_name is not None: + return_value = self._channel_name + if self._program_name is not None: + return_value = return_value + ': ' + self._program_name + return return_value + + @property + def media_content_id(self): + """Content ID of current playing media.""" + return self._channel_name + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + return self._duration + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._braviarc.set_volume_level(volume) + + def turn_on(self): + """Turn the media player on.""" + self._braviarc.turn_on() + + def turn_off(self): + """Turn off media player.""" + self._braviarc.turn_off() + + def volume_up(self): + """Volume up the media player.""" + self._braviarc.volume_up() + + def volume_down(self): + """Volume down media player.""" + self._braviarc.volume_down() + + def mute_volume(self, mute): + """Send mute command.""" + self._braviarc.mute_volume(mute) + + def select_source(self, source): + """Set the input source.""" + if source in self._content_mapping: + uri = self._content_mapping[source] + self._braviarc.play_content(uri) + + 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._braviarc.media_play() + + def media_pause(self): + """Send media pause command to media player.""" + self._playing = False + self._braviarc.media_pause() + + def media_next_track(self): + """Send next track command.""" + self._braviarc.media_next_track() + + def media_previous_track(self): + """Send the previous track command.""" + self._braviarc.media_previous_track() diff --git a/requirements_all.txt b/requirements_all.txt index bafb405b956..36036655c2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -111,6 +111,9 @@ https://github.com/TheRealLink/pythinkingcleaner/archive/v0.0.2.zip#pythinkingcl # homeassistant.components.alarm_control_panel.alarmdotcom https://github.com/Xorso/pyalarmdotcom/archive/0.1.1.zip#pyalarmdotcom==0.1.1 +# homeassistant.components.media_player.braviatv +https://github.com/aparraga/braviarc/archive/0.3.2.zip#braviarc==0.3.2 + # homeassistant.components.media_player.roku https://github.com/bah2830/python-roku/archive/3.1.1.zip#python-roku==3.1.1