from __future__ import unicode_literals import re import time import socket from urllib import request, parse from xml.etree import ElementTree class Remote(): """ Class for initialising communication with, and sending remote commands to a 2012+ LG TV. """ def __init__(self, ip_address, pair_key=None): """ Initialise class with IP and optional pair key. If not pair key provided, then the pair request will be sent to the TV and `.set_pairing_key()` must be called before use. """ self.pair_key = pair_key self.ip_address = ip_address self.__session = None if not self.ip_address: raise Remote.NoTVFound if self.pair_key: self.__session = self.get_session() else: self.request_pair() @classmethod def find_tvs(cls, attempts=10, first_only=False): """ Create a broadcast socket and listen for LG TVs responding. Returns list of IPs unless `first_only` is true, in which case it will return the first TV found. """ request = 'M-SEARCH * HTTP/1.1\r\n' \ 'HOST: 239.255.255.250:1900\r\n' \ 'MAN: "ssdp:discover"\r\n' \ 'MX: 2\r\n' \ 'ST: urn:schemas-upnp-org:device:MediaRenderer:1\r\n\r\n' sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(1) addresses = [] while attempts > 0: sock.sendto(request.encode(), ('239.255.255.250', 1900)) try: response, address = sock.recvfrom(512) except: attempts -= 1 continue if re.search('LG', response.decode()): if first_only: sock.close() return address[0] else: addresses.append(address[0]) attempts -= 1 sock.close() if first_only: raise Remote.NoTVFound else: if len(addresses) == 0: raise Remote.NoTVFound else: return addresses def set_pairing_key(self, pair_key): """ Set the pairing key and initialise the session with the TV """ self.pair_key = pair_key self.__session = self.get_session() def make_request(self, endpoint, content, extra_headers={}): """ POST the XML request to the configured TV and parse the response """ headers = {'Content-Type': 'application/atom+xml'} headers.update(extra_headers) url = parse.urlunparse(("http", self.ip_address + ":8080", endpoint, "", "", "")) req = request.Request(url, data=content.encode(), headers=headers) response = request.urlopen(req) tree = ElementTree.XML(response.read()) return tree def request_pair(self): """ Request for the TV to display the pairing key on-screen """ content = """ AuthKeyReq """ self.make_request('/hdcp/api/auth', content) def get_session(self): """ Request to pair with the TV and return the session ID """ content = """ AuthReq {0} """.format(self.pair_key) response = self.make_request('/hdcp/api/auth', content) return response.find('session').text def send_command(self, code): """ Send a remote control key command. Ignores response for now. """ if self.pair_key is None: raise Remote.NoPairingKey content = """ {1} HandleKeyInput {0} """.format(code, self.__session) self.make_request('/hdcp/api/dtv_wifirc', content) def send_multiple(self, codes, delay=0.2): """ Send multiple remote control commands with a delay in between. The delay is required as multiple commands can be ignored if too close together. """ for code in codes: self.send_command(code, self.session) time.sleep(delay) # exceptions class NoPairingKey(Exception): """ Exception raised when no pairing key is present and action requring one is attempted. """ pass class NoTVFound(Exception): """ Exception raised when unable to find any LG TVs on the network """ pass # heavily inspired by https://github.com/dreamcat4/lgremote/blob/master/lgremote commands = {} # all work with 42LW579S-ZD menus = { "STATUS_BAR" : 35, "QUICK_MENU" : 69, "HOME_MENU" : 67, "PREMIUM_MENU" : 89 } # all work with 42LW579S-ZD power_controls = { "POWER" : 8, "SLEEP_MENU" : 14 } # all work with 42LW579S-ZD navigation_controls = { "LEFT" : 7, "RIGHT" : 6, "UP" : 64, "DOWN" : 65, "SELECT" : 68, "BACK" : 40, "EXIT" : 91, "RED" : 114, "GREEN" : 113, "YELLOW" : 99, "BLUE" : 97 } # all work with 42LW579S-ZD keypad_controls = { "0" : 16, "1" : 17, "2" : 18, "3" : 19, "4" : 20, "5" : 21, "6" : 22, "7" : 23, "8" : 24, "9" : 25, "_" : 76, } # untested playback_controls = { "PLAY" : 176, "PAUSE" : 186, "FAST_FORWARD" : 142, "REWIND" : 143, "STOP" : 177, "RECORD" : 189 } # untested tv_controls = { "CHANNEL_UP" : 0, "CHANNEL_DOWN" : 1, "CHANNEL_BACK" : 26, "FAVORITES" : 30, "TELETEXT" : 32, "T_OPT" : 33, "CHANNEL_LIST" : 83, "GUIDE" : 169, "INFO_TOGGLE" : 170, "INFO" : 217, "LIVE_TV" : 158 } # all work with 42LW579S-ZD input_controls = { "TV_RADIO" : 15, "SIMPLINK" : 126, "INPUT" : 11, "COMPONENT_RGB_HDMI" : 152, "RGB" : 213, #215 seems to do the same "COMPONENT" : 191, "ANTENNA" : 214, #240 does the same without channels programmed. 240 could also be CABLE input "HDMI" : 198, "HDMI1" : 206, "HDMI2" : 204, "HDMI3" : 233, "HDMI4" : 218, "AV1" : 90, "AV2" : 208, "AV3" : 209, "USB" : 124, "SLIDESHOW_USB1" : 238, "SLIDESHOW_USB2" : 168 } # should work audio_controls = { "VOLUME_UP" : 2, "VOLUME_DOWN" : 3, "MUTE" : 9, "AUDIO" : 10, "SOUND_MODE" : 82, "SUBTITLE_LANGUAGE" : 57, "AUDIO_DESCRIPTION" : 145, "FACTORY_SOUND_CHECK" : 253 } # all work with 42LW579S-ZD picture_controls = { "AV_MODE" : 48, "PICTURE_MODE" : 77, "RATIO" : 121, "RATIO_4_3" : 118, "RATIO_16_9" : 119, "ENERGY_SAVING" : 149, #only works as expected in quick-menu. Outside of it it sets Energy Saving to Auto and toggles the GREEN_EYE_CHECK menu. Maybe due to previous tinkering with POWER_ONLY mode. "CINEMA_ZOOM" : 175, "3D" : 220, "FACTORY_PICTURE_CHECK" : 252 } # not sure about these self_diagnosis = { "DIAG1" : 164, "DIAG2" : 165, "DIAG3" : 173, "DIAG4" : 188, "DIAG5" : 190, "PICTURE_TEST" : 249 } # see: http://openlgtv.org.ru/wiki/index.php/Hidden_service_modes_/_menus hidden_menues = { "INSTALLATION_MENU" : 207, "POWER_ONLY" : 254, "EZ_ADJUST" : 255, "IN_START" : 251 } HAPPENS_WITHOUT_CONFIRMATION = { "FACTORY_RESET" : 250 }