Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for Philips TVs with jointSPACE API #4157

Merged
merged 6 commits into from
Nov 3, 2016
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ omit =
homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/panasonic_viera.py
homeassistant/components/media_player/pandora.py
homeassistant/components/media_player/philips_js.py
homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py
Expand Down
206 changes: 206 additions & 0 deletions homeassistant/components/media_player/philips_js.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
"""
Media Player component to integrate TVs exposing the Joint Space API.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.philips_js/
"""
import logging
from datetime import timedelta
import json

import requests
import voluptuous as vol

from homeassistant.components.media_player import (
PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF,
SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_MUTE, MediaPlayerDevice)
from homeassistant.const import (
STATE_ON, STATE_OFF, STATE_UNKNOWN, CONF_HOST, CONF_NAME)
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv

_LOGGER = logging.getLogger(__name__)

MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)

SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE

DEFAULT_DEVICE = 'default'
DEFAULT_HOST = '127.0.0.1'
DEFAULT_NAME = 'Philips TV'
DEVICE_BASE_URL = 'http://{0}:1925/1/{1}'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lead to the assumption that the port can not be changed by the user on the TV, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is correct. The API always runs on port 1925.

DEVICE_NAME_URL = 'http://{0}:1925/1/system/name'
DEVICE_INPUT_URL = 'http://{0}:1925/1/input/key'
DEVICE_SRC_SET_URL = 'http://{0}:1925/1/sources/current'

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


# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Philips TV platform."""
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)

add_devices([PhilipsJS(host, name)])


# pylint: disable=too-many-instance-attributes,abstract-method
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too-many-instance-attributes is now globally disabled.

class PhilipsJS(MediaPlayerDevice):
"""Representation of a Philips TV exposing the JointSpace API."""

def __init__(self, host, name):
"""Initialize the Philips TV."""
self._host = host
self._name = name
self._state = STATE_UNKNOWN
self._min_volume = None
self._max_volume = None
self._volume = None
self._muted = False
self._program_name = None
self._channel_name = None
self._source = None
self._source_list = []
self._connfail = 0
self._source_mapping = {}

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

@property
def should_poll(self):
"""Device should be polled."""
return True

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

@property
def state(self):
"""Get the device state. An exception means OFF state."""
try:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should never do any I/O inside a property. It should only return cached values.

if self._connfail:
self._connfail -= 1
self._source = None
return STATE_OFF
requests.get(DEVICE_NAME_URL.format(self._host), timeout=5)
self._state = STATE_ON
return self._state
except requests.exceptions.RequestException:
self._connfail = 5
return STATE_OFF

@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

def select_source(self, source):
"""Set the input source."""
if source in self._source_mapping:
data = dict(id=self._source_mapping[source])
try:
resp = requests.post(DEVICE_SRC_SET_URL.format(self._host),
data=json.dumps(data), timeout=5)
if resp.status_code == 200:
self._source = source
except requests.exceptions.RequestException:
_LOGGER.error('Could not set source to %s', source)
else:
_LOGGER.warning('invalid source: %s', source)

@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
if self._volume is not None:
return self._volume
else:
return None

@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._muted

def turn_off(self):
"""Turn off the device."""
self.send_key('Standby')

def volume_up(self):
"""Send volume up command."""
self.send_key('VolumeUp')

def volume_down(self):
"""Send volume down command."""
self.send_key('VolumeDown')

def mute_volume(self, mute):
"""Send mute command."""
self.send_key('Mute')

@property
def media_title(self):
"""Title of current playing media."""
return self._source

@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest date and update device state."""
try:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Home Assistant should not contain any protocol specific code. Could you extract this into a 3rd party lib and add that as a requirement to this platform.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm aware of that. I did it like this for two reasons: 1. Creating an extra lib for a handful of requests seems like a little overkill. And even more important: 2. The jointSPACE API seems to be a dead project, so I won't expect any changes which would have to be taken care of in this platform. But if you insist, I'll do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done with creating the library. Will do the implementation soon. :)

if self._connfail:
_LOGGER.debug('Conn-Fail: %i', self._connfail)
self._connfail -= 1
return
audiodata = json.loads(requests.get(
DEVICE_BASE_URL.format(self._host, 'audio/volume')).text)
self._min_volume = int(audiodata['min'])
self._max_volume = int(audiodata['max'])
self._volume = audiodata['current']
self._muted = audiodata['muted']
srcid = json.loads(requests.get(
DEVICE_BASE_URL.format(self._host,
'sources/current')).text)['id']
srcdict = json.loads(requests.get(
DEVICE_BASE_URL.format(self._host, 'sources')).text)
self._source = srcdict[srcid]['name']
if not self._source_list:
for srcid in sorted(srcdict):
self._source_list.append(srcdict[srcid]['name'])
self._source_mapping[srcdict[srcid]['name']] = srcid
self._state = STATE_ON
except requests.exceptions.RequestException:
self._connfail = 5
self._state = STATE_OFF
self._source = None

def send_key(self, key):
"""Send key command to TV."""
try:
if self._connfail:
self._connfail -= 1
_LOGGER.debug('Conn-Fail: %i', self._connfail)
return False
data = dict(key=key)
requests.post(DEVICE_INPUT_URL.format(self._host),
data=json.dumps(data), timeout=5)
return True
except requests.exceptions.RequestException:
_LOGGER.error('Could not send key %s', key)
self._connfail = 5
self._state = STATE_OFF
self._source = None
return False