Philips Android TV component


#1

Hi guys,

I want to start by saying I don’t know python :(. I’ve made some investigations regarding the Philips Android TVs ( 2016+ ) and found out that you can control it using the jointspace API ( http://jointspace.sourceforge.net/projectdata/documentation/jasonApi/1/doc/API.html ).

Some useful information I’ve found on the net:

The steps to access this API are:

  1. Get a pairing code from Philips ( https://github.com/suborb/philips_android_tv/blob/master/philips.py#L30 )
  2. After this, you will receive an username and password
  3. Use this username and password to access the jointspace API ( using /6/ instead of /1/ and HTTPS instead of HTTP )

Is there anyone interested in doing such a component?

As mentioned earlier, I don’t know python, however, I can help with other stuff if needed.

Best regards,
Stefan


#2

There is already an implementation for the older models: philips_js
My warpper-library for HASS is here: https://github.com/danielperna84/ha-philipsjs
Since I don’t own any Philips devices anymore I can’t actively help with development. But maybe it’s a useful reference for someone who wants to handle the task. To me it seems like someone would need to kind of merge my work with the code you’ve linked to and make a HASS platform that supports the pairing.


#3

It is also possible to control a Android TV using the Android Debug Bridge.
I don’t know if that’s a better option.


#4

So I recently bought a 2016 Philips Android TV, and am currently working on a custom component to control it - basically I’m using the existing philips_js component as a template, and merging in a modified version of the wrapper library by @danielperna84 to pass the correct API path and credentials as required.

Some features are no longer supported in these models (specifically source/channel functions), so I’ve removed support for those from the component as well. So far I have power off, volume control and mute working, however it would theoretically be possible to send any remote control key press combination you want, so there are possibilities there.

The biggest problem right now is that there no longer appears to be a way to get the “playback” state of the TV via the API, for example to detect whether it’s tuned to a certain channel or source input, so HA would not be contextually aware of when it should show/hide playback controls.

In terms of the pairing process, I’m not doing anything specific to handle that from the component itself so that remains a manual step for now, however I might try and automate this in the future. Currently once I obtain the credentials from the pairing, I pass these into username/password fields that I added to the component in my configuration.

Once I finish tweaking everything and tidy the code up a bit, I’ll post the component here.


#5

As promised, this is the custom component I knocked together. It’s pretty much a merge of the above wrapper library and the existing Philips component, with some minor tweaks to support the new API structure and authentication. Right now the functionality is very basic, pretty much just power off, volume/mute control and basic state monitoring. To use this component you will need to obtain credentials for your TV by spoofing the pairing process, this can be done using this python script.

Example configuration:

  - platform: philips_2016
    name: Philips TV
    host: 192.168.x.x
    username: <user from pairing process>
    password: <pass from pairing process>

Save the below as a python script (ex: philips_2016.py) and dump it into your .homeassistant/custom_components/media_player folder:

"""
Media Player component to integrate TVs exposing the Joint Space API.
Updated to support Android-based Philips TVs manufactured from 2016 onwards.
"""
import homeassistant.helpers.config_validation as cv
import argparse
import json
import random
import requests
import string
import sys
import voluptuous as vol

from base64 import b64encode,b64decode
from Crypto.Hash import SHA, HMAC
from datetime import timedelta, datetime
from homeassistant.components.media_player import (PLATFORM_SCHEMA, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, MediaPlayerDevice)
from homeassistant.const import (
	CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, STATE_OFF, STATE_ON, STATE_UNKNOWN)
from homeassistant.util import Throttle
from requests.auth import HTTPDigestAuth

MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)

SUPPORT_PHILIPS_2016 = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE

DEFAULT_DEVICE = 'default'
DEFAULT_HOST = '127.0.0.1'
DEFAULT_USER = 'user'
DEFAULT_PASS = 'pass'
DEFAULT_NAME = 'Philips TV'
BASE_URL = 'https://{0}:1926/6/{1}'
TIMEOUT = 5.0
CONNFAILCOUNT = 5

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
	vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
	vol.Required(CONF_USERNAME, default=DEFAULT_USER): cv.string,
	vol.Required(CONF_PASSWORD, default=DEFAULT_PASS): 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):
	"""Set up the Philips 2016+ TV platform."""
	name = config.get(CONF_NAME)
	host = config.get(CONF_HOST)
	user = config.get(CONF_USERNAME)
	password = config.get(CONF_PASSWORD)
	tvapi = PhilipsTVBase(host, user, password)
	add_devices([PhilipsTV(tvapi, name)])

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

	def __init__(self, tv, name):
		"""Initialize the TV."""
		self._tv = tv
		self._name = name
		self._state = STATE_UNKNOWN
		self._min_volume = None
		self._max_volume = None
		self._volume = None
		self._muted = False
		self._connfail = 0

	@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_features(self):
		"""Flag media player features that are supported."""
		return SUPPORT_PHILIPS_2016

	@property
	def state(self):
		"""Get the device state. An exception means OFF state."""
		return self._state

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

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

	def turn_off(self):
		"""Turn off the device."""
		self._tv.sendKey('Standby')
		if not self._tv.on:
			self._state = STATE_OFF

	def volume_up(self):
		"""Send volume up command."""
		self._tv.sendKey('VolumeUp')
		if not self._tv.on:
			self._state = STATE_OFF

	def volume_down(self):
		"""Send volume down command."""
		self._tv.sendKey('VolumeDown')
		if not self._tv.on:
			self._state = STATE_OFF

	def mute_volume(self, mute):
		"""Send mute command."""
		self._tv.sendKey('Mute')
		if not self._tv.on:
			self._state = STATE_OFF

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

	@Throttle(MIN_TIME_BETWEEN_UPDATES)
	def update(self):
		"""Get the latest data and update device state."""
		self._tv.update()
		self._min_volume = self._tv.min_volume
		self._max_volume = self._tv.max_volume
		self._volume = self._tv.volume
		self._muted = self._tv.muted
		if self._tv.on:
			self._state = STATE_ON
		else:
			self._state = STATE_OFF

class PhilipsTVBase(object):
	def __init__(self, host, user, password):
		self._host = host
		self._user = user
		self._password = password
		self._connfail = 0
		self.on = None
		self.name = None
		self.min_volume = None
		self.max_volume = None
		self.volume = None
		self.muted = None
		self.sources = None
		self.source_id = None
		self.channels = None
		self.channel_id = None

	def _getReq(self, path):
		try:
			if self._connfail:
				self._connfail -= 1
				return None
			resp = requests.get(BASE_URL.format(self._host, path), verify=False, auth=HTTPDigestAuth(self._user, self._password), timeout=TIMEOUT)
			self.on = True
			return json.loads(resp.text)
		except requests.exceptions.RequestException as err:
			self._connfail = CONNFAILCOUNT
			self.on = False
			return None

	def _postReq(self, path, data):
		try:
			if self._connfail:
				self._connfail -= 1
				return False
			resp = requests.post(BASE_URL.format(self._host, path), data=json.dumps(data), verify=False, auth=HTTPDigestAuth(self._user, self._password), timeout=TIMEOUT)
			self.on = True
			if resp.status_code == 200:
				return True
			else:
				return False
		except requests.exceptions.RequestException as err:
			self._connfail = CONNFAILCOUNT
			self.on = False
			return False

	def update(self):
		self.getName()
		self.getAudiodata()

	def getName(self):
		r = self._getReq('system/name')
		if r:
			self.name = r['name']

	def getAudiodata(self):
		audiodata = self._getReq('audio/volume')
		if audiodata:
			self.min_volume = int(audiodata['min'])
			self.max_volume = int(audiodata['max'])
			self.volume = audiodata['current']
			self.muted = audiodata['muted']
		else:
			self.min_volume = None
			self.max_volume = None
			self.volume = None
			self.muted = None

	def setVolume(self, level):
		if level:
			if self.min_volume != 0 or not self.max_volume:
				self.getAudiodata()
			if not self.on:
				return
			try:
				targetlevel = int(level)
			except ValueError:
				return
			if targetlevel < self.min_volume + 1 or targetlevel > self.max_volume:
				return
			self._postReq('audio/volume', {'current': targetlevel, 'muted': False})
			self.volume = targetlevel

	def sendKey(self, key):
		self._postReq('input/key', {'key': key})

#6

I’ve started doing the same thing with a 55PUS6262/05. This isn’t an android tv, but the api looks similar.

A few things I’ve also noticed:

  • No username/password on mine
  • my guess is /1/system is valid on all api versions, and the major version returned is what should be targeted in http://ip-address:1925/{api-version}/
  • The tv (I think) supports push based updates. :1925/6/notifychange can be used to register a notification. This is what I’ve seen posted to the url: https://hastebin.com/eremoyayad.http - I haven’t really had a look at how requests notifications come through
  • Sources/channels do not appear to be supported - but may be worth looking here: /6/activities/tv and /6/system/epgsource
  • WOL can be used to turn the tv on (I think - the mobile app appears to do it)
  • /6/system/storage, /6/system/storage/status and /6/recordings/list may provide an interesting sensor (I don’t use any of the “tv” features so I can’t test these)
  • channeldb/tv could provide the channel lists (again I don’t use it)

Anyway - if you are in the dev channel it might be worth collaborating?


#7

Also worth mentioning on this tv model https://ip:1926 doesn’t connect.


#8

Hi to all!

I’ve a Philips 49PUS7100/12 and I can’t pair the TV using the script from (https://github.com/suborb/philips_android_tv), I can’t get user/pwd for authenticate, nothing happens in the TV when I execute the script.

air:philips_android_tv-master kulo4$ sudo python2.7 philips.py pair --host 192.168.66.110
Starting pairing request
/usr/local/lib/python2.7/site-packages/urllib3/connectionpool.py:858: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
InsecureRequestWarning)
Traceback (most recent call last):
File “philips.py”, line 118, in
main()
File “philips.py”, line 85, in main
pair(config)
File “philips.py”, line 37, in pair
response = r.json()
File “/usr/local/lib/python2.7/site-packages/requests/models.py”, line 892, in json
return complexjson.loads(self.text, **kwargs)
File “/usr/local/Cellar/python/2.7.13_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/init.py”, line 339, in loads
return _default_decoder.decode(s)
File “/usr/local/Cellar/python/2.7.13_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/decoder.py”, line 364, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File “/usr/local/Cellar/python/2.7.13_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/decoder.py”, line 382, in raw_decode
raise ValueError(“No JSON object could be decoded”)
ValueError: No JSON object could be decoded

Any help? Thanks!


#9

Anybody got a Philips 2016 working ‘fine’ with HA?


#10

Hi guys,

many thanks to Rookeh and everybody who worked on that script!! :slightly_smiling_face:

I just needed to remove the from Crypto.Hash import SHA, HMAC import because I was not able to install the needed stuff on the hassio platform.
Anyways, I got the basic functionality on my 65PUS7601/12 working with this custom component, which means I was able to pair the TV, turn it off and to control the volume.

I would be happy about every new feature you can add to this :wink:


#11

Hi all, I really want this component to work on my homeassistant installation… but I’m using docker on synology.
How can I install this plugin ? or when will it be available in the releases ? Does somebody know this ?

thanks!


#12

I found a list with commands…

Standby
Back
Find
RedColour
GreenColour
YellowColour
BlueColour
Home
VolumeUp
VolumeDown
Mute
Options
Dot
Digit0
Digit1
Digit2
Digit3
Digit4
Digit5
Digit6
Digit7
Digit8
Digit9
Info
CursorUp
CursorDown
CursorLeft
CursorRight
Confirm
Next
Previous
Adjust
WatchTV
Viewmode
Teletext
Subtitle
ChannelStepUp
ChannelStepDown
Source
AmbilightOnOff
PlayPause
Pause
FastForward
Stop
Rewind
Record
Online

I want to edit the custom media player file… but have no idea how to start … somebody wants to help


#13

I’m not able to make the pairing succesfull…
Could somebody try to specify how its done?

I saved the philips_2016.py to a new folder (custom) and in my config, i tried to call it… Not any respons or somewhere to find the possibility.

Help :upside_down_face:
TV: 49PUS6561


#14

I got this working on my 55pus727212, i.e. got the pairing code and could then use the component in home assistant (change volume worked fine).

However, after maybe 5-10 volume adjustment tests, it suddenly stopped working and I have not been able to pair it any more (i.e. nothing happens when doing this). Tried restarting the TV etc, but nothing. Any ideas?


#15

I experienced the same problem, after a minute or 10 the component stopped working because the television stopped responding to the requests. I ran some tests and found that after exactly 100 requests the REST API stopped working. I dug a little deeper with Wireshark and discovered that the problem was caused by the SSL-handshake. At request 101 the client send its SSL “client hello”, but the server did not give a “server hello” back anymore. My guess is that the webserver in the television caches SSL connections in an array of max 100 items.

I’m creating a new version that reuses the SSL connection. The test version that I’m running now appears to be stable this way. I’ve also added channel information, a volume slider en rewind/pause/forward functions. It’s almost done and I will share it very soon.


#16

Nice! Please let me know if you want/need any assistance. How did you get the tv to start accepting connections again?


#17

First I unplugged the TV completely to reset it, but killing the XTV app and switching the TV off and on works on my TV to get it started again.


#18

I put an initial version on Github:

I have a problem to get the “Play” button to appear on the control. If anybody knows what’s wrong with the code please let me know.


#19

Nice, well … also very interessted to get this solved … so just let us know if you need any help in this matter :slight_smile :slight_smile:


#20

I was just trying to set this up too, but was failing from a HASS point of view.
What has to go in configuration.yaml and where does the script have to be saved with what name?