From 5ebcf238c28d41dc157e819f374549f45afce333 Mon Sep 17 00:00:00 2001 From: Tom Hendrikx Date: Sun, 4 Jan 2026 17:38:28 +0100 Subject: [PATCH 1/4] HDCP 'command' needs dtv_wifirc, 'auth' doesn't This resolves authentication issues with HDCP, and proper command sending --- pylgnetcast/pylgnetcast.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pylgnetcast/pylgnetcast.py b/pylgnetcast/pylgnetcast.py index a3c9da9..bf6a7da 100644 --- a/pylgnetcast/pylgnetcast.py +++ b/pylgnetcast/pylgnetcast.py @@ -151,6 +151,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): def send_command(self, command): """Send remote control commands to the TV.""" + _LOGGER.debug(f"send_command: command={command}") message = self.COMMAND % ( self._session, LG_HANDLE_KEY_INPUT, @@ -253,6 +254,7 @@ 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): @@ -261,7 +263,8 @@ def _display_pair_key(self): 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: + _LOGGER.debug(f"_send_to_tv: message_type={message_type}, message={message}, payload={payload}") + if message_type == "command" and self.protocol == LG_PROTOCOL.HDCP: message_type = "dtv_wifirc" url = "%s%s" % (self.url, message_type) if message: From e7bbc8af0e73b01216f2986c3b45e4d1b6a6de20 Mon Sep 17 00:00:00 2001 From: Tom Hendrikx Date: Sun, 4 Jan 2026 18:00:46 +0100 Subject: [PATCH 2/4] Log failed command sending --- pylgnetcast/pylgnetcast.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pylgnetcast/pylgnetcast.py b/pylgnetcast/pylgnetcast.py index bf6a7da..015b96f 100644 --- a/pylgnetcast/pylgnetcast.py +++ b/pylgnetcast/pylgnetcast.py @@ -157,7 +157,9 @@ def send_command(self, command): LG_HANDLE_KEY_INPUT, "%s" % 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.""" From 429841256226ba28060a42e5aecce19dd5549572 Mon Sep 17 00:00:00 2001 From: Tom Hendrikx Date: Sun, 4 Jan 2026 20:42:13 +0100 Subject: [PATCH 3/4] Replace weird header juggling with local variables --- pylgnetcast/pylgnetcast.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/pylgnetcast/pylgnetcast.py b/pylgnetcast/pylgnetcast.py index 015b96f..79b3105 100644 --- a/pylgnetcast/pylgnetcast.py +++ b/pylgnetcast/pylgnetcast.py @@ -163,15 +163,7 @@ def send_command(self, command): 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 @@ -203,6 +195,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 @@ -263,19 +256,21 @@ 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.""" - _LOGGER.debug(f"_send_to_tv: message_type={message_type}, message={message}, payload={payload}") + _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 From 7bae6342544f9dc6bddb5bf7fb2ab18775565169 Mon Sep 17 00:00:00 2001 From: Tom Hendrikx Date: Sun, 4 Jan 2026 21:11:33 +0100 Subject: [PATCH 4/4] Add specific commands for HDCP protocol, allow translation between the two. This helps in keeping upstream functionality simple. You can use ROAP commands and they will be translated to HDCP automatically (as far as supported). Use `client.send_command(2, command_protocol=LG_PROTOCOL.HDCP)` when you're sending native HDCP commands. --- pylgnetcast/pylgnetcast.py | 95 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/pylgnetcast/pylgnetcast.py b/pylgnetcast/pylgnetcast.py index 79b3105..bbaad03 100644 --- a/pylgnetcast/pylgnetcast.py +++ b/pylgnetcast/pylgnetcast.py @@ -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 @@ -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.""" @@ -149,9 +207,14 @@ 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}") + _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, @@ -274,6 +337,32 @@ def _send_to_tv(self, message_type, message=None, payload=None, udap=False): ) 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."""