Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 105 additions & 16 deletions pylgnetcast/pylgnetcast.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@


class LG_COMMAND(object):
"""LG TV remote control commands."""
"""LG TV remote control commands for ROAP protocol."""

POWER = 1
NUMBER_0 = 2
Expand Down Expand Up @@ -105,6 +105,64 @@ class LG_COMMAND(object):
SWITCH_VIDEO = 416
APPS = 417

class LG_COMMAND_HDCP(object):
"""LG TV remote control commands for HDCP protocol."""

POWER = 8
NUMBER_0 = 16
NUMBER_1 = 17
NUMBER_2 = 18
NUMBER_3 = 19
NUMBER_4 = 20
NUMBER_5 = 21
NUMBER_6 = 22
NUMBER_7 = 23
NUMBER_8 = 24
NUMBER_9 = 25
MUTE_TOGGLE = 9
HOME_MENU = 67
DASH = 76
FLASHBACK = 26
CHANNEL_LIST = 83
OK = 68
CHANNEL_UP = 0
CHANNEL_DOWN = 1
VOLUME_UP = 2
VOLUME_DOWN = 3
UP = 64
DOWN = 65
LEFT = 7
RIGHT = 6
BACK = 40
EXIT = 91
CONFIRM = 68
QUICK_MENU = 69
RED = 114
GREEN = 113
YELLOW = 99
BLUE = 97
LIVE_TV = 158
STOP = 177
PLAY = 176
PAUSE = 186
SKIP_BACKWARD = 143
SKIP_FORWARD = 142
RECORD = 189
EPG = 169
ENERGY_SAVING = 149
AV_MODE = 48
EXTERNAL_INPUT = 11
FAVORITE_CHANNEL = 30
SIMPLINK = 126
ASPECT_RATIO = 121
PROGRAM_INFORMATION = 170
NETCAST = 89
GUIDE = 169
SHOW_SUBTITLE = 57
TELE_TEXT = 32
TEXT_OPTION = 33
AUDIO_DESCRIPTION = 145


class LG_QUERY(object):
"""LG TV data queries."""
Expand Down Expand Up @@ -149,26 +207,26 @@ def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager method to support with statement."""
self._session = None

def send_command(self, command):
def send_command(self, command, command_protocol=LG_PROTOCOL.ROAP):
"""Send remote control commands to the TV."""
_LOGGER.debug(f"send_command: {command=} {command_protocol=}")
if self.protocol != command_protocol:
command = self._translate_command(command, command_protocol, self.protocol)
if command is None:
return

message = self.COMMAND % (
self._session,
LG_HANDLE_KEY_INPUT,
"<value>%s</value>" % command,
)
self._send_to_tv("command", message)
response = self._send_to_tv("command", message)
if response.status_code != requests.codes.ok:
_LOGGER.error(f"Failed to send command {command} to TV.")

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
response = self._send_to_tv("data", payload={"target": "rootservice.xml"}, udap=True)
if response.status_code != requests.codes.ok:
return None
data = response.text
Expand Down Expand Up @@ -200,6 +258,7 @@ def change_channel(self, channel):

def query_data(self, query):
"""Query status information from the TV."""
# TODO: UDAP: yes or no?
response = self._send_to_tv("data", payload={"target": query})
if response.status_code == requests.codes.ok:
data = response.text
Expand Down Expand Up @@ -253,27 +312,57 @@ def _get_session_id(self):
data = response.text
tree = ElementTree.XML(data)
session = tree.find("session").text
_LOGGER.info(f"Session established: session={session}")
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):
def _send_to_tv(self, message_type, message=None, payload=None, udap=False):
"""Send message of given type to the tv."""
if message_type != "command" and self.protocol == LG_PROTOCOL.HDCP:
_LOGGER.debug(f"_send_to_tv: {message_type=}, {message=}, {payload=}, {udap=}")
if message_type == "command" and self.protocol == LG_PROTOCOL.HDCP:
message_type = "dtv_wifirc"

url = "%s%s" % (self.url, message_type)
headers = {**self.HEADER, "User-Agent": "UDAP/2.0"} if udap else self.HEADER
if message:
response = requests.post(
url, data=message, headers=self.HEADER, timeout=DEFAULT_TIMEOUT
url, data=message, headers=headers, timeout=DEFAULT_TIMEOUT
)
else:
response = requests.get(
url, params=payload, headers=self.HEADER, timeout=DEFAULT_TIMEOUT
url, params=payload, headers=headers, timeout=DEFAULT_TIMEOUT
)
return response

@staticmethod
def _translate_command(command, from_protocol, to_protocol):
"""
Translate command from one protocol to another.

It would be better if commands were previously implemented as strings instead of integers,
then this would a lot less ugly. But yay for legacy :)
"""
_LOGGER.debug(f"_translate_command: {command=}, {from_protocol=}, {to_protocol=}")
if from_protocol == LG_PROTOCOL.ROAP and to_protocol == LG_PROTOCOL.HDCP:
source = LG_COMMAND
target = LG_COMMAND_HDCP
else:
source = LG_COMMAND_HDCP
target = LG_COMMAND

for attr in dir(target):
if not attr.startswith("__"):
if getattr(source, attr, None) == command:
translated_command = getattr(target, attr, None)
_LOGGER.info(f"Translated {from_protocol} command {command} to {to_protocol}: {translated_command}")
return translated_command

_LOGGER.error(f"Command {command} not supported for {to_protocol} protocol.")
return None


class LgNetCastError(Exception):
"""Base class for all exceptions in this module."""
Expand Down