"""
Client library for the LG Smart TV running NetCast 3 or 4.
LG Smart TV models released in 2012 (NetCast 3.0) and LG Smart TV models
released in 2013 (NetCast 4.0) are supported.
For pre 2012 LG TV remote commands are supported by the "hdcp" protocol.
The client is inspired by the work of
https://github.com/ubaransel/lgcommander
"""
import logging
import requests
import time
from xml.etree import ElementTree
_LOGGER = logging.getLogger(__name__)
__all__ = [
"LgNetCastClient",
"LG_COMMAND",
"LG_QUERY",
"LgNetCastError",
"AccessTokenError",
"SessionIdError",
]
# LG TV handler
LG_HANDLE_KEY_INPUT = "HandleKeyInput"
LG_HANDLE_MOUSE_MOVE = "HandleTouchMove"
LG_HANDLE_MOUSE_CLICK = "HandleTouchClick"
LG_HANDLE_TOUCH_WHEEL = "HandleTouchWheel"
LG_HANDLE_CHANNEL_CHANGE = "HandleChannelChange"
LG_CHANGE_INPUT_SOURCE = "ChangeInputSource"
DEFAULT_PORT = 8080
DEFAULT_TIMEOUT = 3
class LG_COMMAND(object):
"""LG TV remote control commands."""
POWER = 1
NUMBER_0 = 2
NUMBER_1 = 3
NUMBER_2 = 4
NUMBER_3 = 5
NUMBER_4 = 6
NUMBER_5 = 7
NUMBER_6 = 8
NUMBER_7 = 9
NUMBER_8 = 10
NUMBER_9 = 11
UP = 12
DOWN = 13
LEFT = 14
RIGHT = 15
OK = 20
HOME_MENU = 21
BACK = 23
VOLUME_UP = 24
VOLUME_DOWN = 25
MUTE_TOGGLE = 26
CHANNEL_UP = 27
CHANNEL_DOWN = 28
BLUE = 29
GREEN = 30
RED = 31
YELLOW = 32
PLAY = 33
PAUSE = 34
STOP = 35
FAST_FORWARD = 36
REWIND = 37
SKIP_FORWARD = 38
SKIP_BACKWARD = 39
RECORD = 40
RECORDING_LIST = 41
REPEAT = 42
LIVE_TV = 43
EPG = 44
PROGRAM_INFORMATION = 45
ASPECT_RATIO = 46
EXTERNAL_INPUT = 47
PIP_SECONDARY_VIDEO = 48
SHOW_SUBTITLE = 49
PROGRAM_LIST = 50
TELE_TEXT = 51
MARK = 52
VIDEO_3D = 400
LR_3D = 401
DASH = 402
PREVIOUS_CHANNEL = 403
FAVORITE_CHANNEL = 404
QUICK_MENU = 405
TEXT_OPTION = 406
AUDIO_DESCRIPTION = 407
ENERGY_SAVING = 409
AV_MODE = 410
SIMPLINK = 411
EXIT = 412
RESERVATION_PROGRAM_LIST = 413
PIP_CHANNEL_UP = 414
PIP_CHANNEL_DOWN = 415
SWITCH_VIDEO = 416
APPS = 417
class LG_QUERY(object):
"""LG TV data queries."""
CUR_CHANNEL = "cur_channel"
CHANNEL_LIST = "channel_list"
CONTEXT_UI = "context_ui"
VOLUME_INFO = "volume_info"
SCREEN_IMAGE = "screen_image"
IS_3D = "is_3d"
class LG_PROTOCOL(object):
"""Supported LG TV protcols."""
HDCP = "hdcp"
ROAP = "roap"
class LgNetCastClient(object):
"""LG NetCast TV client using the ROAP or HDCP protocol."""
HEADER = {"Content-Type": "application/atom+xml"}
XML = ''
KEY = XML + "AuthKeyReq"
AUTH = XML + "%s%s"
COMMAND = XML + "%s%s%s"
def __init__(self, host, access_token, protocol=LG_PROTOCOL.ROAP):
"""Initialize the LG TV client."""
self.url = "http://%s:%s/%s/api/" % (host, DEFAULT_PORT, protocol)
self.access_token = access_token
self.protocol = protocol
self._session = None
def __enter__(self):
"""Context manager method to support with statement."""
self._session = self._get_session_id()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager method to support with statement."""
self._session = None
def send_command(self, command):
"""Send remote control commands to the TV."""
message = self.COMMAND % (
self._session,
LG_HANDLE_KEY_INPUT,
"%s" % command,
)
self._send_to_tv("command", message)
def query_device_info(self):
"""Get model information about the TV."""
# We're using UDAP to retrieve this information, which requires a specific User-Agent.
# As self.HEADER is a class variable, we will make a copy to ensure we don't interfere
# with other instances
try:
self.HEADER = {**self.HEADER, "User-Agent": "UDAP/2.0"}
response = self._send_to_tv("data", payload={"target": "rootservice.xml"})
finally:
# Remove the instance version of self.HEADER to restore original functionality
del self.HEADER
if response.status_code != requests.codes.ok:
return None
data = response.text
tree = ElementTree.fromstring(data.encode("utf-8"))
return {
"uuid": tree.findtext("device/uuid"),
"model_name": tree.findtext("device/modelName"),
"friendly_name": tree.findtext("device/friendlyName")
}
def change_input_source(self, type, index):
"""Send change input source command to the TV."""
message = self.COMMAND % (
self._session,
LG_CHANGE_INPUT_SOURCE,
"%d%d" % (type, index),
)
self._send_to_tv("command", message)
def change_channel(self, channel):
"""Send change channel command to the TV."""
message = self.COMMAND % (
self._session,
LG_HANDLE_CHANNEL_CHANGE,
ElementTree.tostring(channel, encoding="utf-8"),
)
self._send_to_tv("command", message)
def query_data(self, query):
"""Query status information from the TV."""
response = self._send_to_tv("data", payload={"target": query})
if response.status_code == requests.codes.ok:
data = response.text
tree = ElementTree.fromstring(data.encode("utf-8"))
data_list = []
for data in tree.iter("data"):
data_list.append(data)
return data_list
def get_volume(self):
"""Get volume info from the TV."""
volume_info = self.query_data("volume_info")
if volume_info:
volume_info = volume_info[0]
volume = float(volume_info.find("level").text)
muted = volume_info.find("mute").text == "true"
return volume, muted
def set_volume(self, volume):
"""Try to set a specific volume level."""
for _ in range(2):
current_volume, _ = self.get_volume()
self._set_volume_diff(volume, current_volume)
def _set_volume_diff(self, volume, current_volume):
"""Simulate setting a specific volume level based on the difference."""
volume_difference = volume - current_volume
if volume_difference != 0:
command = (
LG_COMMAND.VOLUME_UP
if volume_difference > 0
else LG_COMMAND.VOLUME_DOWN
)
for _ in range(abs(int(volume_difference))):
self.send_command(command)
time.sleep(0.45)
def _get_session_id(self):
"""Get the session key for the TV connection.
If a pair key is defined the session id is requested otherwise display
the pair key on TV.
"""
if not self.access_token:
self._display_pair_key()
raise AccessTokenError("No access token specified to create session.")
message = self.AUTH % ("AuthReq", self.access_token)
response = self._send_to_tv("auth", message)
if response.status_code != requests.codes.ok:
raise SessionIdError("Can not get session id from TV.")
data = response.text
tree = ElementTree.XML(data)
session = tree.find("session").text
return session
def _display_pair_key(self):
"""Send message to display the pair key on TV screen."""
self._send_to_tv("auth", self.KEY)
def _send_to_tv(self, message_type, message=None, payload=None):
"""Send message of given type to the tv."""
if message_type != "command" and self.protocol == LG_PROTOCOL.HDCP:
message_type = "dtv_wifirc"
url = "%s%s" % (self.url, message_type)
if message:
response = requests.post(
url, data=message, headers=self.HEADER, timeout=DEFAULT_TIMEOUT
)
else:
response = requests.get(
url, params=payload, headers=self.HEADER, timeout=DEFAULT_TIMEOUT
)
return response
class LgNetCastError(Exception):
"""Base class for all exceptions in this module."""
class AccessTokenError(LgNetCastError):
"""No access token specified to create session."""
class SessionIdError(LgNetCastError):
"""No session id could be retrieved from TV."""