python-lgtv/lg.py

239 lines
5.6 KiB
Python

from __future__ import unicode_literals
import re
import time
import socket
import httplib
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
if not self.ip_address:
raise Remote.NoTVFound
if self.pair_key:
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, ('239.255.255.250', 1900))
try:
response, address = sock.recvfrom(512)
except:
attempts -= 1
continue
if re.search('LG', response):
if first_only:
sock.close()
return address[0]
else:
addresses.append(address[0])
attempts -= 1
sock.close()
if first_only:
raise NoTVFound
else:
if len(addresses) == 0:
raise 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.get_session()
def make_request(self, endpoint, content, extra_headers={}):
"""
POST the XML request to the configured TV and parse the response
"""
http = httplib.HTTPConnection(self.ip_address, port=8080)
headers = {'Content-Type': 'application/atom+xml'}
headers.update(extra_headers)
http.request("POST", endpoint, content, headers=headers)
response = http.getresponse()
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('/roap/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('/roap/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>
<name>HandleKeyInput</name>
<value>{0}</value>
</command>
""".format(code)
self.make_request('/roap/api/command', 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)
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
# command code shortcuts
POWER = 1
NUM_0 = 2
NUM_1 = 3
NUM_2 = 4
NUM_3 = 5
NUM_4 = 6
NUM_5 = 7
NUM_6 = 8
NUM_7 = 9
NUM_8 = 10
NUM_9 = 11
UP = 12
DOWN = 13
LEFT = 14
RIGHT = 15
OK = 20
HOME = 21
MENU = 22
BACK = 23
VOLUME_UP = 24
VOLUME_DOWN = 25
MUTE = 26
CHANNEL_UP = 27
CHANNEL_DOWN = 28
BLUE = 29
GREEN = 30
RED = 31
YELLOW = 32
PLAY = 33
PAUSE = 34
STOP = 35
FF = 36
REW = 37
SKIP_FF = 38
SKIP_REW = 39
REC = 40
REC_LIST = 41
LIVE = 43
EPG = 44
INFO = 45
ASPECT = 46
EXT = 47
PIP = 48
SUBTITLE = 49
PROGRAM_LIST = 50
TEXT = 51
MARK = 52
_3D = 400
_3D_LR = 401
DASH = 402
PREV = 403
FAV = 404
QUICK_MENU = 405
TEXT_OPTION = 406
AUDIO_DESC = 407
NETCAST = 408
ENERGY_SAVE = 409
AV = 410
SIMPLINK = 411
EXIT = 412
RESERVE = 413
PIP_CHANNEL_UP = 414
PIP_CHANNEL_DOWN = 415
PIP_SWITCH = 416
APPS = 417