2014-07-04 00:39:52 +02:00
from __future__ import unicode_literals
import re
2014-07-04 10:27:23 +02:00
import time
2014-07-04 00:39:52 +02:00
import socket
2019-01-31 18:56:19 +01:00
from urllib import request , parse
2014-07-04 00:39:52 +02:00
from xml . etree import ElementTree
class Remote ( ) :
2014-07-04 10:27:23 +02:00
"""
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 .
"""
2014-07-04 00:39:52 +02:00
self . pair_key = pair_key
2014-07-04 10:27:23 +02:00
self . ip_address = ip_address
2019-01-31 19:31:11 +01:00
self . __session = None
2014-07-04 10:27:23 +02:00
2014-07-04 00:39:52 +02:00
if not self . ip_address :
raise Remote . NoTVFound
if self . pair_key :
2019-01-31 19:31:11 +01:00
self . __session = self . get_session ( )
2014-07-04 00:39:52 +02:00
else :
self . request_pair ( )
2014-07-04 10:27:23 +02:00
@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 .
"""
2014-07-04 00:39:52 +02:00
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 )
2014-07-04 10:27:23 +02:00
sock . settimeout ( 1 )
2014-07-04 00:39:52 +02:00
2014-07-04 10:27:23 +02:00
addresses = [ ]
while attempts > 0 :
2019-01-31 18:56:19 +01:00
sock . sendto ( request . encode ( ) , ( ' 239.255.255.250 ' , 1900 ) )
2014-07-04 00:39:52 +02:00
try :
response , address = sock . recvfrom ( 512 )
except :
2014-07-04 10:27:23 +02:00
attempts - = 1
2014-07-04 00:39:52 +02:00
continue
2019-01-31 18:56:19 +01:00
if re . search ( ' LG ' , response . decode ( ) ) :
2014-07-04 10:27:23 +02:00
if first_only :
sock . close ( )
return address [ 0 ]
else :
addresses . append ( address [ 0 ] )
2014-07-04 00:39:52 +02:00
2014-07-04 10:27:23 +02:00
attempts - = 1
2014-07-04 00:39:52 +02:00
sock . close ( )
2014-07-04 10:27:23 +02:00
if first_only :
2014-07-04 10:32:03 +02:00
raise Remote . NoTVFound
2014-07-04 10:27:23 +02:00
else :
if len ( addresses ) == 0 :
2014-07-04 10:32:03 +02:00
raise Remote . NoTVFound
2014-07-04 10:27:23 +02:00
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
2019-01-31 19:31:11 +01:00
self . __session = self . get_session ( )
2014-07-04 00:39:52 +02:00
def make_request ( self , endpoint , content , extra_headers = { } ) :
2014-07-04 10:27:23 +02:00
"""
POST the XML request to the configured TV and parse the response
"""
2014-07-04 00:39:52 +02:00
headers = { ' Content-Type ' : ' application/atom+xml ' }
headers . update ( extra_headers )
2019-01-31 18:56:19 +01:00
url = parse . urlunparse ( ( " http " , self . ip_address + " :8080 " , endpoint , " " , " " , " " ) )
req = request . Request ( url , data = content . encode ( ) , headers = headers )
response = request . urlopen ( req )
2014-07-04 00:39:52 +02:00
tree = ElementTree . XML ( response . read ( ) )
return tree
def request_pair ( self ) :
2014-07-04 10:27:23 +02:00
"""
Request for the TV to display the pairing key on - screen
"""
2014-07-04 00:39:52 +02:00
content = """
< ? xml version = " 1.0 " encoding = " utf-8 " ? >
< auth >
< type > AuthKeyReq < / type >
< / auth >
"""
2019-01-31 18:10:45 +01:00
self . make_request ( ' /hdcp/api/auth ' , content )
2014-07-04 00:39:52 +02:00
def get_session ( self ) :
2014-07-04 10:27:23 +02:00
"""
Request to pair with the TV and return the session ID
"""
2014-07-04 00:39:52 +02:00
content = """
< ? xml version = " 1.0 " encoding = " utf-8 " ? >
< auth >
< type > AuthReq < / type >
< value > { 0 } < / value >
< / auth >
""" .format(self.pair_key)
2019-01-31 18:10:45 +01:00
response = self . make_request ( ' /hdcp/api/auth ' , content )
2014-07-04 00:39:52 +02:00
return response . find ( ' session ' ) . text
2019-01-31 19:31:11 +01:00
def send_command ( self , code ) :
2014-07-04 10:27:23 +02:00
"""
Send a remote control key command . Ignores response for now .
"""
if self . pair_key is None :
raise Remote . NoPairingKey
2014-07-04 00:39:52 +02:00
content = """
< ? xml version = " 1.0 " encoding = " utf-8 " ? >
< command >
2019-01-31 18:10:45 +01:00
< session > { 1 } < / session >
< type > HandleKeyInput < / type >
2014-07-04 00:39:52 +02:00
< value > { 0 } < / value >
< / command >
2019-01-31 19:31:11 +01:00
""" .format(code, self.__session)
2019-01-31 18:10:45 +01:00
self . make_request ( ' /hdcp/api/dtv_wifirc ' , content )
2014-07-04 00:39:52 +02:00
2014-07-04 10:27:23 +02:00
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 :
2019-01-31 18:10:45 +01:00
self . send_command ( code , self . session )
2014-07-04 10:27:23 +02:00
time . sleep ( delay )
2014-07-04 00:39:52 +02:00
# exceptions
class NoPairingKey ( Exception ) :
2014-07-04 10:27:23 +02:00
"""
Exception raised when no pairing key is present and action requring one
is attempted .
"""
2014-07-04 00:39:52 +02:00
pass
class NoTVFound ( Exception ) :
2014-07-04 10:27:23 +02:00
"""
Exception raised when unable to find any LG TVs on the network
"""
2014-07-04 00:39:52 +02:00
pass
2019-01-31 18:10:45 +01:00
# 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 }