diff --git a/pylgnetcast/pylgnetcast.py b/pylgnetcast/pylgnetcast.py index a3c9da9..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,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, "%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.""" - # 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 @@ -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 @@ -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."""