diff --git a/fauxmo.py b/fauxmo.py
new file mode 100644
index 0000000..1df4e1d
--- /dev/null
+++ b/fauxmo.py
@@ -0,0 +1,423 @@
+#!/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
+import requests
+import select
+import socket
+import struct
+import sys
+import time
+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
+
+
+"""
+
+
+DEBUG = False
+
+def dbg(msg):
+ global DEBUG
+ if DEBUG:
+ print msg
+ sys.stdout.flush()
+
+
+# A simple utility class to wait for incoming data to be
+# ready on a socket.
+
+class poller:
+ def __init__(self):
+ if 'poll' in dir(select):
+ self.use_poll = True
+ self.poller = select.poll()
+ else:
+ self.use_poll = False
+ self.targets = {}
+
+ 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):
+ if not fileno:
+ fileno = target.fileno()
+ if self.use_poll:
+ self.poller.unregister(fileno)
+ del(self.targets[fileno])
+
+ def poll(self, timeout = 0):
+ if self.use_poll:
+ ready = self.poller.poll(timeout)
+ else:
+ ready = []
+ if len(self.targets) > 0:
+ (rlist, wlist, xlist) = select.select(self.targets.keys(), [], [], timeout)
+ ready = [(x, None) for x in rlist]
+ for one_ready in ready:
+ 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
+# selection.
+
+class upnp_device(object):
+ this_host_ip = None
+
+ @staticmethod
+ def local_ip_address():
+ if not upnp_device.this_host_ip:
+ temp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ try:
+ temp_socket.connect(('8.8.8.8', 53))
+ upnp_device.this_host_ip = temp_socket.getsockname()[0]
+ except:
+ upnp_device.this_host_ip = '127.0.0.1'
+ 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):
+ self.listener = listener
+ self.poller = poller
+ self.port = port
+ self.root_url = root_url
+ self.server_version = server_version
+ self.persistent_uuid = persistent_uuid
+ self.uuid = uuid.uuid4()
+ self.other_headers = other_headers
+
+ if ip_address:
+ self.ip_address = ip_address
+ else:
+ self.ip_address = upnp_device.local_ip_address()
+
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.socket.bind((self.ip_address, self.port))
+ self.socket.listen(5)
+ if self.port == 0:
+ self.port = self.socket.getsockname()[1]
+ self.poller.add(self)
+ self.client_sockets = {}
+ self.listener.add_device(self)
+
+ def fileno(self):
+ return self.socket.fileno()
+
+ def do_read(self, fileno):
+ if fileno == self.socket.fileno():
+ (client_socket, client_address) = self.socket.accept()
+ self.poller.add(self, client_socket.fileno())
+ self.client_sockets[client_socket.fileno()] = client_socket
+ else:
+ data, sender = self.client_sockets[fileno].recvfrom(4096)
+ if not data:
+ self.poller.remove(self, fileno)
+ del(self.client_sockets[fileno])
+ else:
+ self.handle_request(data, sender, self.client_sockets[fileno])
+
+ def handle_request(self, data, sender, socket):
+ pass
+
+ def get_name(self):
+ return "unknown"
+
+ def respond_to_search(self, destination, search_target):
+ 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))
+ 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)
+ temp_socket.sendto(message, destination)
+
+
+# This subclass does the bulk of the work to mimic a WeMo switch on the network.
+
+class fauxmo(upnp_device):
+ @staticmethod
+ 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):
+ 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)
+ if action_handler:
+ self.action_handler = action_handler
+ else:
+ self.action_handler = self
+ dbg("FauxMo device '%s' ready on %s:%s" % (self.name, self.ip_address, self.port))
+
+ 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}
+ date_str = email.utils.formatdate(timeval=None, localtime=False, usegmt=True)
+ message = ("HTTP/1.1 200 OK\r\n"
+ "CONTENT-LENGTH: %d\r\n"
+ "CONTENT-TYPE: text/xml\r\n"
+ "DATE: %s\r\n"
+ "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"
+ "CONNECTION: close\r\n"
+ "\r\n"
+ "%s" % (len(xml), date_str, xml))
+ socket.send(message)
+ elif data.find('SOAPACTION: "urn:Belkin:service:basicevent:1#SetBinaryState"') != -1:
+ success = False
+ if data.find('1') != -1:
+ # on
+ dbg("Responding to ON for %s" % self.name)
+ success = self.action_handler.on()
+ elif data.find('0') != -1:
+ # off
+ dbg("Responding to OFF for %s" % self.name)
+ success = self.action_handler.off()
+ 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)
+ 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))
+ socket.send(message)
+ else:
+ dbg(data)
+
+ def on(self):
+ return False
+
+ def off(self):
+ return True
+
+
+# Since we have a single process managing several virtual UPnP devices,
+# we only need a single listener for UPnP broadcasts. When a matching
+# search is received, it causes each device instance to respond.
+#
+# Note that this is currently hard-coded to recognize only the search
+# from the Amazon Echo for WeMo devices. In particular, it does not
+# support the more common root device general search. The Echo
+# doesn't search for root devices.
+
+class upnp_broadcast_responder(object):
+ TIMEOUT = 0
+
+ def __init__(self):
+ self.devices = []
+
+ def init_socket(self):
+ ok = True
+ 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)
+
+ #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))
+ 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)
+ ok = False
+
+ except Exception, e:
+ dbg("Failed to initialize UPnP sockets:",e)
+ return False
+ if ok:
+ dbg("Listening for UPnP broadcasts")
+
+ def fileno(self):
+ return self.ssock.fileno()
+
+ 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:
+ 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):
+ if self.TIMEOUT:
+ self.ssock.setblocking(0)
+ ready = select.select([self.ssock], [], [], self.TIMEOUT)[0]
+ else:
+ self.ssock.setblocking(1)
+ ready = True
+
+ try:
+ if ready:
+ return self.ssock.recvfrom(size)
+ else:
+ return False, False
+ except Exception, e:
+ dbg(e)
+ return False, False
+
+ def add_device(self, device):
+ self.devices.append(device)
+ dbg("UPnP broadcast listener: new device registered")
+
+
+# This is an example handler class. The fauxmo class expects handlers to be
+# instances of objects that have on() and off() methods that return True
+# on success and False otherwise.
+#
+# This example class takes two full URLs that should be requested when an on
+# and off command are invoked respectively. It ignores any return data.
+
+class rest_api_handler(object):
+ def __init__(self, on_cmd, off_cmd):
+ self.on_cmd = on_cmd
+ self.off_cmd = off_cmd
+
+ def on(self):
+ r = requests.get(self.on_cmd)
+ return r.status_code == 200
+
+ def off(self):
+ r = requests.get(self.off_cmd)
+ return r.status_code == 200
+
+
+# Each entry is a list with the following elements:
+#
+# name of the virtual switch
+# object with 'on' and 'off' methods
+# port # (optional; may be omitted)
+
+# NOTE: As of 2015-08-17, the Echo appears to have a hard-coded limit of
+# 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')],
+]
+
+
+if len(sys.argv) > 1 and sys.argv[1] == '-d':
+ DEBUG = True
+
+# Set up our singleton for polling the sockets for data ready
+p = poller()
+
+# Set up our singleton listener for UPnP broadcasts
+u = upnp_broadcast_responder()
+u.init_socket()
+
+# Add the UPnP broadcast listener to the poller so we can respond
+# when a broadcast is received.
+p.add(u)
+
+# Create our FauxMo virtual switch devices
+for one_faux in FAUXMOS:
+ 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])
+
+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
+