diff --git a/fauxmo.py b/fauxmo.py old mode 100755 new mode 100644 index 1df4e1d..9bbdac3 --- a/fauxmo.py +++ b/fauxmo.py @@ -1,29 +1,5 @@ #!/usr/bin/env python -""" -The MIT License (MIT) - -Copyright (c) 2015 Maker Musings - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - # For a complete discussion, see http://www.makermusings.com import email.utils @@ -36,34 +12,42 @@ import urllib import uuid - - # This XML is the minimum needed to define one of our virtual switches # to the Amazon Echo SETUP_XML = """ - - - urn:MakerMusings:device:controllee:1 - %(device_name)s - Belkin International Inc. - Emulated Socket - 3.1415 - uuid:Socket-1_0-%(device_serial)s - + + + urn:Belkin:device:controllee:1 + %(device_name)s + Belkin International Inc. + Emulated Socket + 1.0 + uuid:Socket-1_0-%(device_serial)s + """ +GETBINARY_XML = """ + + + + %(binary_state)s + + + +""" -DEBUG = False +DEBUG = True def dbg(msg): global DEBUG if DEBUG: - print msg + print(msg) sys.stdout.flush() + # A simple utility class to wait for incoming data to be # ready on a socket. @@ -76,21 +60,21 @@ def __init__(self): self.use_poll = False self.targets = {} - def add(self, target, fileno = None): + def add(self, target, fileno=None): if not fileno: fileno = target.fileno() if self.use_poll: self.poller.register(fileno, select.POLLIN) self.targets[fileno] = target - def remove(self, target, fileno = None): + def remove(self, target, fileno=None): if not fileno: fileno = target.fileno() if self.use_poll: self.poller.unregister(fileno) - del(self.targets[fileno]) + del (self.targets[fileno]) - def poll(self, timeout = 0): + def poll(self, timeout=0): if self.use_poll: ready = self.poller.poll(timeout) else: @@ -102,7 +86,7 @@ def poll(self, timeout = 0): target = self.targets.get(one_ready[0], None) if target: target.do_read(one_ready[0]) - + # Base class for a generic UPnP device. This is far from complete # but it supports either specified or automatic IP address and port @@ -120,12 +104,12 @@ def local_ip_address(): upnp_device.this_host_ip = temp_socket.getsockname()[0] except: upnp_device.this_host_ip = '127.0.0.1' - del(temp_socket) + del (temp_socket) dbg("got local address of %s" % upnp_device.this_host_ip) return upnp_device.this_host_ip - - def __init__(self, listener, poller, port, root_url, server_version, persistent_uuid, other_headers = None, ip_address = None): + def __init__(self, listener, poller, port, root_url, server_version, persistent_uuid, other_headers=None, + ip_address=None): self.listener = listener self.poller = poller self.port = port @@ -161,7 +145,7 @@ def do_read(self, fileno): data, sender = self.client_sockets[fileno].recvfrom(4096) if not data: self.poller.remove(self, fileno) - del(self.client_sockets[fileno]) + del (self.client_sockets[fileno]) else: self.handle_request(data, sender, self.client_sockets[fileno]) @@ -170,28 +154,32 @@ def handle_request(self, data, sender, socket): def get_name(self): return "unknown" - + def respond_to_search(self, destination, search_target): - dbg("Responding to search for %s" % self.get_name()) + # dbg("Responding to search for %s" % self.get_name()) date_str = email.utils.formatdate(timeval=None, localtime=False, usegmt=True) - location_url = self.root_url % {'ip_address' : self.ip_address, 'port' : self.port} - message = ("HTTP/1.1 200 OK\r\n" - "CACHE-CONTROL: max-age=86400\r\n" - "DATE: %s\r\n" - "EXT:\r\n" - "LOCATION: %s\r\n" - "OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n" - "01-NLS: %s\r\n" - "SERVER: %s\r\n" - "ST: %s\r\n" - "USN: uuid:%s::%s\r\n" % (date_str, location_url, self.uuid, self.server_version, search_target, self.persistent_uuid, search_target)) + location_url = self.root_url % {'ip_address': self.ip_address, 'port': self.port} + message = ('HTTP/1.1 200 OK\r\n' + 'CACHE-CONTROL: max-age=86400\r\n' + 'DATE: %s\r\n' + 'EXT:\r\n' + 'LOCATION: %s\r\n' + 'OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n' + '01-NLS: %s\r\n' + 'SERVER: %s\r\n' + 'ST: %s\r\n' + 'USN: uuid:%s::%s\r\n' % ( + date_str, location_url, self.uuid, self.server_version, search_target, self.persistent_uuid, + search_target)) if self.other_headers: for header in self.other_headers: message += "%s\r\n" % header message += "\r\n" temp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + message = bytes(message, 'utf-8') + # dbg("Responding with: " + str(message) + "\n") temp_socket.sendto(message, destination) - + # This subclass does the bulk of the work to mimic a WeMo switch on the network. @@ -200,13 +188,16 @@ class fauxmo(upnp_device): def make_uuid(name): return ''.join(["%x" % sum([ord(c) for c in name])] + ["%x" % ord(c) for c in "%sfauxmo!" % name])[:14] - def __init__(self, name, listener, poller, ip_address, port, action_handler = None): + def __init__(self, name, listener, poller, ip_address, port, action_handler=None): + self.binaryState = 0 self.serial = self.make_uuid(name) self.name = name self.ip_address = ip_address persistent_uuid = "Socket-1_0-" + self.serial other_headers = ['X-User-Agent: redsonic'] - upnp_device.__init__(self, listener, poller, port, "http://%(ip_address)s:%(port)s/setup.xml", "Unspecified, UPnP/1.0, Unspecified", persistent_uuid, other_headers=other_headers, ip_address=ip_address) + upnp_device.__init__(self, listener, poller, port, "http://%(ip_address)s:%(port)s/setup.xml", + "Unspecified, UPnP/1.0, Unspecified", persistent_uuid, other_headers=other_headers, + ip_address=ip_address) if action_handler: self.action_handler = action_handler else: @@ -217,9 +208,11 @@ def get_name(self): return self.name def handle_request(self, data, sender, socket): - if data.find('GET /setup.xml HTTP/1.1') == 0: - dbg("Responding to setup.xml for %s" % self.name) - xml = SETUP_XML % {'device_name' : self.name, 'device_serial' : self.serial} + if data.find(b'GET /setup.xml HTTP/1.1') == 0: + # dbg("Receiving " + str(data) + "\n") + # dbg("Responding to setup.xml for %s" % self.name) + xml = SETUP_XML % {'device_name': self.name, 'device_serial': self.serial} + location_url = self.root_url % {'ip_address': self.ip_address, 'port': self.port} date_str = email.utils.formatdate(timeval=None, localtime=False, usegmt=True) message = ("HTTP/1.1 200 OK\r\n" "CONTENT-LENGTH: %d\r\n" @@ -228,46 +221,61 @@ def handle_request(self, data, sender, socket): "LAST-MODIFIED: Sat, 01 Jan 2000 00:01:15 GMT\r\n" "SERVER: Unspecified, UPnP/1.0, Unspecified\r\n" "X-User-Agent: redsonic\r\n" + "LOCATION: %s\r\n" "CONNECTION: close\r\n" "\r\n" - "%s" % (len(xml), date_str, xml)) + "%s" % (len(xml), date_str, location_url,xml)) + message = bytes(message, 'utf-8') + dbg("Responding with " + str(message) + "\n") socket.send(message) - elif data.find('SOAPACTION: "urn:Belkin:service:basicevent:1#SetBinaryState"') != -1: + elif data.find(b'SOAPACTION: "urn:Belkin:service:basicevent:1#SetBinaryState"') != -1: success = False - if data.find('1') != -1: + if data.find(b'1') != -1: # on dbg("Responding to ON for %s" % self.name) success = self.action_handler.on() - elif data.find('0') != -1: + if success: + self.binaryState = 1 + elif data.find(b'0') != -1: # off - dbg("Responding to OFF for %s" % self.name) + # dbg("Responding to OFF for %s" % self.name) success = self.action_handler.off() + if success: + self.binaryState = 0 else: dbg("Unknown Binary State request:") dbg(data) if success: - # The echo is happy with the 200 status code and doesn't - # appear to care about the SOAP response body - soap = "" - date_str = email.utils.formatdate(timeval=None, localtime=False, usegmt=True) + xml = GETBINARY_XML % {'binary_state': self.binaryState} message = ("HTTP/1.1 200 OK\r\n" "CONTENT-LENGTH: %d\r\n" "CONTENT-TYPE: text/xml charset=\"utf-8\"\r\n" - "DATE: %s\r\n" - "EXT:\r\n" - "SERVER: Unspecified, UPnP/1.0, Unspecified\r\n" - "X-User-Agent: redsonic\r\n" - "CONNECTION: close\r\n" "\r\n" - "%s" % (len(soap), date_str, soap)) + "%s" % (len(xml), xml)) + message = bytes(message, 'utf-8') socket.send(message) + + elif data.find(b'SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"') != -1: + dbg("Receiving " + str(data) + "\n") + dbg("Responding to GetBinaryState for %s" % self.name) + xml = GETBINARY_XML % {'binary_state': self.binaryState} + message = ("HTTP/1.1 200 OK\r\n" + "CONTENT-LENGTH: %d\r\n" + "CONTENT-TYPE: text/xml; charset=\"utf-8\"\r\n" + "\r\n" + "%s" % (len(xml), xml)) + message = bytes(message, 'utf-8') + dbg("Responding with " + str(message) + "\n") + socket.send(message) else: dbg(data) def on(self): + self.binaryState = 1 return False def off(self): + self.binaryState = 0 return True @@ -291,27 +299,27 @@ def init_socket(self): self.ip = '239.255.255.250' self.port = 1900 try: - #This is needed to join a multicast group - self.mreq = struct.pack("4sl",socket.inet_aton(self.ip),socket.INADDR_ANY) + # This is needed to join a multicast group + self.mreq = struct.pack("4sl", socket.inet_aton(self.ip), socket.INADDR_ANY) - #Set up server socket - self.ssock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM,socket.IPPROTO_UDP) - self.ssock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) + # Set up server socket + self.ssock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + self.ssock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: - self.ssock.bind(('',self.port)) - except Exception, e: - dbg("WARNING: Failed to bind %s:%d: %s" , (self.ip,self.port,e)) + self.ssock.bind(('', self.port)) + except Exception as e: + dbg("WARNING: Failed to bind %s:%d: %s", (self.ip, self.port, e)) ok = False try: - self.ssock.setsockopt(socket.IPPROTO_IP,socket.IP_ADD_MEMBERSHIP,self.mreq) - except Exception, e: - dbg('WARNING: Failed to join multicast group:',e) + self.ssock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, self.mreq) + except Exception as e: + dbg('WARNING: Failed to join multicast group:', e) ok = False - except Exception, e: - dbg("Failed to initialize UPnP sockets:",e) + except Exception as e: + dbg("Failed to initialize UPnP sockets:", e) return False if ok: dbg("Listening for UPnP broadcasts") @@ -322,15 +330,17 @@ def fileno(self): def do_read(self, fileno): data, sender = self.recvfrom(1024) if data: - if data.find('M-SEARCH') == 0 and data.find('urn:Belkin:device:**') != -1: + if not data.find(b"NOTIFY *") == 0: # I had bunch of other devices that were sending NOTIFY * and it was annoying, So you can remove it if it doesn't happens with you + dbg("Receiving " + str(data) + "\n") + if data.find(b'M-SEARCH') == 0 and data.find(b'upnp:rootdevice') != -1: # upnp:rootdevice # urn:Belkin:device:** for device in self.devices: time.sleep(0.1) device.respond_to_search(sender, 'urn:Belkin:device:**') else: pass - #Receive network data - def recvfrom(self,size): + # Receive network data + def recvfrom(self, size): if self.TIMEOUT: self.ssock.setblocking(0) ready = select.select([self.ssock], [], [], self.TIMEOUT)[0] @@ -343,7 +353,7 @@ def recvfrom(self,size): return self.ssock.recvfrom(size) else: return False, False - except Exception, e: + except Exception as e: dbg(e) return False, False @@ -373,6 +383,19 @@ def off(self): return r.status_code == 200 +class dummy_handler(object): + def __init__(self, name): + self.name = name + + def on(self): + print(self.name, "ON") + return True + + def off(self): + print(self.name, "OFF") + return True + + # Each entry is a list with the following elements: # # name of the virtual switch @@ -383,12 +406,11 @@ def off(self): # 16 switches it can control. Only the first 16 elements of the FAUXMOS # list will be used. -FAUXMOS = [ - ['office lights', rest_api_handler('http://192.168.5.4/ha-api?cmd=on&a=office', 'http://192.168.5.4/ha-api?cmd=off&a=office')], - ['kitchen lights', rest_api_handler('http://192.168.5.4/ha-api?cmd=on&a=kitchen', 'http://192.168.5.4/ha-api?cmd=off&a=kitchen')], +FAUXMOS = [ # 54001 and 54002 are ports + ['office lights', dummy_handler('office lights'), 54001], + ['kitchen lights', dummy_handler('kitchen lights'),54002], ] - if len(sys.argv) > 1 and sys.argv[1] == '-d': DEBUG = True @@ -408,16 +430,12 @@ def off(self): if len(one_faux) == 2: # a fixed port wasn't specified, use a dynamic one one_faux.append(0) - switch = fauxmo(one_faux[0], u, p, None, one_faux[2], action_handler = one_faux[1]) + switch = fauxmo(one_faux[0], u, p, None, one_faux[2], action_handler=one_faux[1]) dbg("Entering main loop\n") while True: - try: - # Allow time for a ctrl-c to stop the process - p.poll(100) - time.sleep(0.1) - except Exception, e: - dbg(e) - break + # Allow time for a ctrl-c to stop the process + p.poll(100) + time.sleep(0.1)