From d63de71130d7ec46c21792ac7e05eff435d11008 Mon Sep 17 00:00:00 2001 From: Ryan Grieve Date: Fri, 4 Jul 2014 09:27:23 +0100 Subject: [PATCH] adding documentation, changing pairing flow and cleaning up --- README.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++- example.py | 10 ++--- lg.py | 98 +++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 195 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 43e2efa..2ae5d43 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,107 @@ python-lgtv =========== -Simple class for pairing with and controlling your 2012+ LG TV with python. Example included. +Simple class for pairing with and controlling your 2012+ LG TV with python. + +You can find all TVs on the network using the `find_tvs` class method. + +``` +from lg import Remote + +addresses = Remote.find_tvs() +``` +If you have only one, it'll be faster to use the `first_only` parameter. + +``` +address = Remote.find_tvs(first_only=True) +``` + +You will need a pairing key, if you already know it you can pass it to the `Remote` class' `__init__` other wise you can reate your remote control instance and it'll request it from the TV. You can then provide this pairing key using the `set_pairing_key` method. + +``` +remote = Remote(address) +# Pairing key will appear on screen +key = raw_input('Please enter pairing key: ') +remote.set_pairing_key(key) +``` + +After this you can send single or multiple commands using `send_command` and `send_multiple`. + +``` +remote.send_command(Remote.VOLUME_UP) + +commands = [Remote.HOME, Remote.EXIT, Remote.MENU] +remote.send_multiple(commands) +``` + +An optional `delay` parameter can be provided to `send_multiple` this will the amount of seconds the control will wait between commands. N.B. Sending commands too fast can cause some of them to be ignored. + +A reference of all the shortcut commands available are below, you are free to send any integer in `send_command` but be wary if you don't know what your are doing. + +``` + 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 +``` diff --git a/example.py b/example.py index 2d03203..11398fc 100644 --- a/example.py +++ b/example.py @@ -1,10 +1,10 @@ from lg import Remote if __name__ == "__main__": - try: - Remote() - except Remote.NoPairingKey: - key = raw_input('Enter pairing key: ') - remote = Remote(key) + address = Remote.find_tvs(first_only=True) + remote = Remote(address) + + key = raw_input('Enter pairing key: ') + remote.set_pairing_key(key) remote.send_command(Remote.VOLUME_UP) diff --git a/lg.py b/lg.py index 1667459..b8f9182 100644 --- a/lg.py +++ b/lg.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import re +import time import socket import httplib @@ -8,10 +9,21 @@ 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. + """ - def __init__(self, pair_key=None): self.pair_key = pair_key - self.ip_address = self.find_tv() + self.ip_address = ip_address + if not self.ip_address: raise Remote.NoTVFound @@ -19,9 +31,15 @@ class Remote(): self.get_session() else: self.request_pair() - raise Remote.NoPairingKey - def find_tv(self, retries=10): + @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' \ @@ -29,26 +47,48 @@ class Remote(): 'ST: urn:schemas-upnp-org:device:MediaRenderer:1\r\n\r\n' sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.settimeout(3) + sock.settimeout(1) - while retries > 0: + addresses = [] + while attempts > 0: sock.sendto(request, ('239.255.255.250', 1900)) try: response, address = sock.recvfrom(512) except: - retries -= 1 + attempts -= 1 continue if re.search('LG', response): - sock.close() - return address[0] + if first_only: + sock.close() + return address[0] + else: + addresses.append(address[0]) - retries -= 1 + attempts -= 1 sock.close() - return None + 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) @@ -58,6 +98,10 @@ class Remote(): return tree def request_pair(self): + """ + Request for the TV to display the pairing key on-screen + """ + content = """ @@ -67,6 +111,10 @@ class Remote(): self.make_request('/roap/api/auth', content) def get_session(self): + """ + Request to pair with the TV and return the session ID + """ + content = """ @@ -78,6 +126,12 @@ class Remote(): 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 = """ @@ -87,15 +141,35 @@ class Remote(): """.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 codes + # command code shortcuts POWER = 1 NUM_0 = 2