├── README.md ├── example.py ├── parsing.py ├── pyviera.py └── viera.py /README.md: -------------------------------------------------------------------------------- 1 | # PyViera 2 | 3 | ## What? 4 | PyViera allows you control your Panasonic VIERA TV programmatically. 5 | 6 | ## Which TVs are compatible? 7 | Taken directly from the 'Compatible VIERA models section' from the description of the Viera remote app on the App Store: 8 | 9 | <<2011 Models (Series)>> 10 | North America: VT30, GT30, ST30, PST34, GT31, DT30, D30 11 | Latin America: VT30, GT30, ST30, DT30, E30 12 | Europe/CIS: VT30, GT30, GW30, GTX34, GTN33, GTF32, GTS31, G30, ST30, ST31, ST33, S30, S31, UT30, DT35, DT30, D35, D30, E30, E31, EX34, EN33, EF32, ES31, EW30 13 | Australia/New Zealand: VT30, GT30, ST30, DT30, E30 14 | Malaysia/Thai Land/Singapore/Indonesia/Middle East/Iran: VT30, ST30, DT30, E30 15 | Vietnam/Philippines: VT30, ST30, DT30 16 | India: VT30, ST30, E30 17 | Saudi Arabia: VT30, ST30, UT30 18 | South Africa: VT30, UT30 19 | China: VT30, VT31, GT30, GT31, GT32, ST30, ST32, S30, DT30 20 | Hong Kong: ST30 21 | Taiwan: VT30, ST30, E30 22 | Japan: VT3, GT3, ST3, DT3 23 | (As of July 22, 2011) 24 | 25 | ## How? 26 | In a nutshell: 27 | 28 | ```python 29 | from pyviera import VieraFinder 30 | vf = VieraFinder() 31 | tv = vf.get_viera() 32 | tv.num(18) 33 | tv.mute() 34 | tv.mute() # Toggles 35 | tv.vol_up() 36 | ``` 37 | 38 | Check out the `Viera` class in `viera.py` for all the supported keys. They're all pretty self-explanatory. 39 | 40 | ## Contributors 41 | Many thanks to: 42 | - [Tenderfoot14](https://github.com/Tenderfoot14) for adding lots of new SOAP codes. 43 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from pyviera import VieraFinder 2 | 3 | if __name__ == '__main__': 4 | vf = VieraFinder() 5 | 6 | tv = vf.get_viera() 7 | 8 | tv.mute() 9 | -------------------------------------------------------------------------------- /parsing.py: -------------------------------------------------------------------------------- 1 | import urlparse 2 | 3 | from lxml import objectify 4 | 5 | from viera import Viera 6 | 7 | class NoServiceDescriptionError(StandardError): pass 8 | 9 | def parse_discovery_response(data): 10 | for line in data.splitlines(): 11 | parts = line.split(': ') 12 | if len(parts) > 1 and parts[0] == 'LOCATION': 13 | return parts[1] 14 | 15 | def parse_description(data, abs_url): 16 | desc = objectify.fromstring(data) 17 | 18 | try: 19 | service = desc.device.serviceList.service 20 | 21 | service_type = service.serviceType.text 22 | control_url= urlparse.urljoin(abs_url, service.controlURL.text) 23 | 24 | # Unused urls 25 | #service_id = urlparse.urljoin(abs_url, service.serviceId.text) 26 | #scpd_url = urlparse.urljoin(abs_url, service.SCPDURL.text) 27 | #event_url = urlparse.urljoin(abs_url, service.eventSubURL.text) 28 | 29 | hostname = urlparse.urlparse(abs_url).netloc 30 | 31 | return Viera( 32 | hostname, 33 | control_url, 34 | service_type, 35 | ) 36 | except AttributeError: 37 | raise NoServiceDescriptionError 38 | -------------------------------------------------------------------------------- /pyviera.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | from urllib2 import urlopen 4 | 5 | from parsing import parse_discovery_response, parse_description 6 | 7 | IFACE = '0.0.0.0' 8 | SSDP_MCAST_ADDR = '239.255.255.250' 9 | SSDP_PORT = 1900 10 | 11 | class VieraFinder(object): 12 | 13 | def __init__(self): 14 | desc_url = self.discover() 15 | 16 | desc = urlopen(desc_url).read() 17 | self.viera = parse_description(desc, desc_url) 18 | 19 | def get_viera(self): 20 | return self.viera 21 | 22 | def create_new_listener(self, ip, port): 23 | newsock = socket.socket( 24 | socket.AF_INET, 25 | socket.SOCK_DGRAM, 26 | socket.IPPROTO_UDP 27 | ) 28 | newsock.setsockopt( 29 | socket.SOL_SOCKET, 30 | socket.SO_REUSEADDR, 31 | 1, 32 | ) 33 | newsock.bind((ip, port)) 34 | return newsock 35 | 36 | def discover(self): 37 | header = 'M-SEARCH * HTTP/1.1' 38 | fields = ( 39 | ('ST', 'urn:panasonic-com:device:p00RemoteController:1'), 40 | ('MX', '1'), 41 | ('MAN', '"ssdp:discover"'), 42 | ('HOST', '239.255.255.250:1900'), 43 | ) 44 | 45 | p = self._make_packet(header, fields) 46 | 47 | sock = self.create_new_listener(IFACE, SSDP_PORT) 48 | sock.sendto(p, (SSDP_MCAST_ADDR, SSDP_PORT)) 49 | 50 | data = sock.recv(1024) 51 | 52 | location = parse_discovery_response(data) 53 | 54 | return location 55 | 56 | def _make_packet(self, header, fields): 57 | return '\r\n'.join([header] + [': '.join(pair) for pair in fields]) + '\r\n' 58 | -------------------------------------------------------------------------------- /viera.py: -------------------------------------------------------------------------------- 1 | import urllib2 2 | 3 | class Viera(object): 4 | def __init__(self, hostname, control_url, service_type): 5 | self.hostname = hostname 6 | self.control_url = control_url 7 | self.service_type = service_type 8 | 9 | self.sendkey_action = Action('X_SendKey', ('X_KeyEvent',)) 10 | 11 | def _sendkey(self, slug): 12 | req = self.sendkey_action.to_soap_request( 13 | self.control_url, 14 | self.hostname, 15 | self.service_type, 16 | (slug,), 17 | ) 18 | 19 | urllib2.urlopen(req).read() 20 | 21 | def __unicode__(self): 22 | return u'' % ( 23 | self.hostname, 24 | self.control_url, 25 | self.service_type, 26 | ) 27 | 28 | def vol_up(self): 29 | self._sendkey('NRC_VOLUP-ONOFF') 30 | 31 | def vol_down(self): 32 | self._sendkey('NRC_VOLDOWN-ONOFF') 33 | 34 | def mute(self): 35 | self._sendkey('NRC_MUTE-ONOFF') 36 | 37 | def num(self, number): 38 | for digit in str(number): 39 | self._sendkey('NRC_D%s-ONOFF' % digit) 40 | 41 | def power(self): 42 | self._sendkey('NRC_TV-ONOFF') 43 | 44 | def toggle_3D(self): 45 | self._sendkey('NRC_3D-ONOFF') 46 | 47 | def toggle_SDCard(self): 48 | self._sendkey('NRC_SD_CARD-ONOFF') 49 | 50 | def red(self): 51 | self._sendkey('NRC_RED-ONOFF') 52 | 53 | def green(self): 54 | self._sendkey('NRC_GREEN-ONOFF') 55 | 56 | def yellow(self): 57 | self._sendkey('NRC_YELLOW-ONOFF') 58 | 59 | def blue(self): 60 | self._sendkey('NRC_BLUE-ONOFF') 61 | 62 | def vtools(self): 63 | self._sendkey('NRC_VTOOLS-ONOFF') 64 | 65 | def cancel(self): 66 | self._sendkey('NRC_CANCEL-ONOFF') 67 | 68 | def option(self): 69 | self._sendkey('NRC_SUBMENU-ONOFF') 70 | 71 | def Return(self): 72 | self.sendkey('NRC_RETURN-ONOFF') 73 | 74 | def enter(self): 75 | self._sendkey('NRC_ENTER-ONOFF') 76 | 77 | def right(self): 78 | self._sendkey('NRC_RIGHT-ONOFF') 79 | 80 | def left(self): 81 | self._sendkey('NRC_LEFT-ONOFF') 82 | 83 | def up(self): 84 | self._sendkey('NRC_UP-ONOFF') 85 | 86 | def down(self): 87 | self._sendkey('NRC_DOWN-ONOFF') 88 | 89 | def display(self): 90 | self._sendkey('NRC_DISP_MODE-ONOFF') 91 | 92 | def menu(self): 93 | self._sendkey('NRC_MENU-ONOFF') 94 | 95 | def connect(self): 96 | self._sendkey('NRC_INTERNET-ONOFF') 97 | 98 | def link(self): 99 | self._sendkey('NRC_VIERA_LINK-ONOFF') 100 | 101 | def guide(self): 102 | self._sendkey('NRC_EPG-ONOFF') 103 | 104 | def text(self): 105 | self._sendkey('NRC_TEXT-ONOFF') 106 | 107 | def subtitles(self): 108 | self._sendkey('NRC_STTL-ONOFF') 109 | 110 | def info(self): 111 | self._sendkey('NRC_INFO-ONOFF') 112 | 113 | def index(self): 114 | self._sendkey('NRC_INDEX-ONOFF') 115 | 116 | def hold(self): 117 | self._sendkey('NRC_HOLD-ONOFF') 118 | 119 | class Action(object): 120 | def __init__(self, name, arguments): 121 | self.name = name 122 | self.arguments = arguments 123 | 124 | def to_soap_request(self, url, hostname, service_type, values): 125 | assert len(values) == len(self.arguments) 126 | 127 | params = ''.join(['<%s>%s' % (arg, value, arg) for arg, value in zip(self.arguments, values)]) 128 | 129 | soap_body = ( 130 | '' 131 | '' 132 | '' 133 | '' 134 | '%(params)s' 135 | '' 136 | '' 137 | '' 138 | ) % { 139 | 'method_name': self.name, 140 | 'service_type': service_type, 141 | 'params': params, 142 | } 143 | 144 | headers = { 145 | 'Host': hostname, 146 | 'Content-Length': len(soap_body), 147 | 'Content-Type': 'text/xml', 148 | 'SOAPAction': '"%s#%s"' % (service_type, self.name), 149 | } 150 | 151 | req = urllib2.Request(url, soap_body, headers) 152 | 153 | return req 154 | --------------------------------------------------------------------------------