311 lines
8.0 KiB
Python
311 lines
8.0 KiB
Python
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 = """
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<auth>
|
|
<type>AuthKeyReq</type>
|
|
</auth>
|
|
"""
|
|
self.make_request('/hdcp/api/auth', content)
|
|
|
|
def get_session(self):
|
|
"""
|
|
Request to pair with the TV and return the session ID
|
|
"""
|
|
|
|
content = """
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<auth>
|
|
<type>AuthReq</type>
|
|
<value>{0}</value>
|
|
</auth>
|
|
""".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 = """
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<command>
|
|
<session>{1}</session>
|
|
<type>HandleKeyInput</type>
|
|
<value>{0}</value>
|
|
</command>
|
|
""".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 }
|