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})