Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow bridging Lifx bulbs to the Echo via the Hue interface #3

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,25 @@ the Amazon Echo with your own home automation.
The Amazon Echo will allow you to control a limited number of home automation devices
by voice. If you want to control device types that it doesn't know about, or perform
more sophisticated actions, the Echo doesn't provide any native options. This code
emulates the Belkin WeMo devices in software, allowing you to have it appear that
any number of them are on your network and to link their on and off actions to
any code you want.
emulates the Belkin WeMo and Philips Hue devices in software, allowing you to have it
appear that any number of them are on your network and to link their on and off actions
to any code you want.

### Instructions

All of the code to make it work is contained in the single file, `fauxmo.py`. It
requires Python 2.7 and standard libraries. The example handler class that
reacts to on and off commands uses the [python-requests](http://docs.python-requests.org/en/latest/)
library, but could be replaced with code that does the same thing in many
different ways.
different ways. The example handler for Philips Hue devices communicates with Lifx
bulbs via the [Lazylights v2.0 API](https://github.com/mpapi/lazylights/tree/2.0)

Copy the fauxmo.py file to your server and edit the FAUXMOS list for the device names
you want and the URLs to invoke for on and off commands for each one. You can execute it
simply as `./fauxmo.py`. If you want debug output, execute `./fauxmo.py -d`. If you
want it to run for an extended period, you could do something like `nohup ./fauxmo.py &`
or take extra steps to make it run at startup, etc.
you want and the URLs to invoke for on and off commands for each one. To work with Lifx
devices, remove the existing entries from FAUXMOS and allow Lazylights to discover bulbs.
You can execute it simply as `./fauxmo.py`. If you want debug output, execute
`./fauxmo.py -d`. If you want it to run for an extended period, you could do something like
`nohup ./fauxmo.py &` or take extra steps to make it run at startup, etc.

**Note:** unless you specify port numbers in the creation of your fauxmo objetcs, your
virtual switch devices will use a different port every time you run fauxmo.py, which will
Expand Down
204 changes: 197 additions & 7 deletions fauxmo.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
# For a complete discussion, see http://www.makermusings.com

import email.utils
import json
import lazylights
import requests
import select
import socket
Expand Down Expand Up @@ -54,6 +56,36 @@
</root>
"""

HUE_SETUP_XML = """<?xml version="1.0" encoding="UTF-8" ?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<URLBase>%(host)%(port)/</URLBase>
<device>
<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>
<friendlyName>Philips hue (##URLBASE##)</friendlyName>
<manufacturer>Royal Philips Electronics</manufacturer>
<manufacturerURL>http://www.philips.com</manufacturerURL>
<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>
<modelName>Philips hue bridge 2012</modelName>
<modelNumber>929000226503</modelNumber>
<modelURL>http://www.meethue.com</modelURL>
<serialNumber>0017880ae670</serialNumber>
<UDN>uuid:2f402f80-da50-11e1-9b23-0017880ae670</UDN>
<serviceList>
<service>
<serviceType>(null)</serviceType>
<serviceId>(null)</serviceId>
<controlURL>(null)</controlURL>
<eventSubURL>(null)</eventSubURL>
<SCPDURL>(null)</SCPDURL>
</service>
</serviceList>
<presentationURL>index.html</presentationURL>
</device>
</root>"""

DEBUG = False

Expand Down Expand Up @@ -125,13 +157,14 @@ def local_ip_address():
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, protocol, 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.protocol = protocol
self.uuid = uuid.uuid4()
self.other_headers = other_headers

Expand Down Expand Up @@ -171,6 +204,9 @@ def handle_request(self, data, sender, socket):
def get_name(self):
return "unknown"

def get_protocol(self):
return self.protocol

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)
Expand All @@ -191,7 +227,128 @@ def respond_to_search(self, destination, search_target):
message += "\r\n"
temp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
temp_socket.sendto(message, destination)



# This subclass implements Philips Hue compatibility

class fauxhue(upnp_device):
@staticmethod
def make_uuid(name):
return ''.join(["%x" % sum([ord(c) for c in name])] + ["%x" % ord(c) for c in "%sfauxhue!" % name])[:14]

def __init__(self, name, listener, poller, ip_address, port, action_handler = None):
self.lights = {}
self.privates = {}
self.ip_address = ip_address
self.serial = self.make_uuid(name)
self.name = name
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/description.xml", "Unspecified, UPnP/1.0, Unspecified", persistent_uuid, "hue", other_headers=other_headers, ip_address=ip_address)
if action_handler:
self.action_handler = action_handler
else:
self.action_handler = self
dbg("FauxHue device '%s' ready on %s:%s" % (self.name, self.ip_address, self.port))

def add_bulb (self, name, state=False, brightness=0, private=None):
light = {
"state": {
"on": state,
"bri": brightness,
"hue": 0,
"sat": 0,
"xy": [0.0000, 0.0000],
"ct": 0,
"alert": "none",
"effect": "none",
"colormode": "hs",
"reachable": True
},
"type": "Extended color light",
"name": name,
"modelid": "LCT001",
"swversion": "65003148",
"pointsymbol": {
"1": "none",
"2": "none",
"3": "none",
"4": "none",
"5": "none",
"6": "none",
"7": "none",
"8": "none"
}
}
lightnum = len(self.lights) + 1
self.lights[str(lightnum)] = light
self.privates[str(lightnum)] = private

def get_name(self):
return self.name

def send(self, socket, data):
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(data), date_str, data))
dbg(message)
socket.send(message)

def handle_request(self, data, sender, socket):
tokens = data.split()
if len(tokens) < 3 or tokens[2] != 'HTTP/1.1':
dbg("Unknown request %s\n" % data)
return
requestdata = tokens[1].split('/')
if tokens[0] == 'GET':
if requestdata[1] == 'description.xml':
dbg("Responding to description.xml for %s" % self.name)
xml = HUE_SETUP_XML % {'host' : self.ip_address, 'port' : self.port}
self.send(socket, xml)
elif len(requestdata) == 4 and requestdata[3] == 'lights':
data = json.dumps(self.lights)
self.send(socket, data)
elif len(requestdata) >= 5 and requestdata[3] == 'lights':
data = json.dumps(self.lights[requestdata[4]])
self.send(socket, data)
elif tokens[0] == 'PUT':
if len(requestdata) >= 5 and requestdata[3] == 'lights':
light = requestdata[4]
submission = data.split('\n')[6]
command = json.loads(submission)
responses = []
for setting in command.keys():
value = command[setting]
private = self.privates[light]
self.lights[light]['state'][setting] = value
dbg ("Set %s to %s\n" % (setting, value))
if setting == "on":
if value == True:
self.action_handler.on(private)
elif value == False:
self.action_handler.off(private)
elif setting == "bri":
self.action_handler.dim(private, value)
apistring = "/lights/%s/state/%s" % (light, setting)
responses.append({"success":{apistring : command[setting]}})
self.send(socket, json.dumps(responses))
else:
dbg("Unknown request: %s" % data)

def on(self):
return False

def off(self):
return True


# This subclass does the bulk of the work to mimic a WeMo switch on the network.

Expand All @@ -206,7 +363,7 @@ def __init__(self, name, listener, poller, ip_address, port, action_handler = No
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, "wemo", other_headers=other_headers, ip_address=ip_address)
if action_handler:
self.action_handler = action_handler
else:
Expand Down Expand Up @@ -264,10 +421,10 @@ def handle_request(self, data, sender, socket):
else:
dbg(data)

def on(self):
def on(self, private):
return False

def off(self):
def off(self, private):
return True


Expand Down Expand Up @@ -324,8 +481,14 @@ def do_read(self, fileno):
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:**')
if device.get_protocol() == "wemo":
time.sleep(0.1)
device.respond_to_search(sender, 'urn:Belkin:device:**')
elif data.find('M-SEARCH') == 0 and data.find('urn:schemas-upnp-org:device:basic:1') != -1:
for device in self.devices:
if device.get_protocol() == "hue":
time.sleep(0.1)
device.respond_to_search(sender, 'urn:schemas-upnp-org:device:basic:1')
else:
pass

Expand Down Expand Up @@ -373,6 +536,24 @@ def off(self):
return r.status_code == 200


# Lifx handler for the Philips Hue compatibility. The fauxhue class expects
# handlers to be instances of objects that have on(), off() and dim()
# methods that return True on success and False otherwise.

class lifx_api_handler(object):
def on(self, bulb):
lazylights.set_power([bulb.bulb], True)
return True

def off(self, bulb):
lazylights.set_power([bulb.bulb], False)
return True

def dim(self, bulb, value):
lazylights.set_state([bulb.bulb], bulb.hue, bulb.saturation, value * 65535 / 255, bulb.kelvin, 1000, raw=True)
return True


# Each entry is a list with the following elements:
#
# name of the virtual switch
Expand Down Expand Up @@ -410,6 +591,15 @@ def off(self):
one_faux.append(0)
switch = fauxmo(one_faux[0], u, p, None, one_faux[2], action_handler = one_faux[1])

bulbs = lazylights.find_bulbs(timeout=10)
if len(bulbs) > 0:
lifx = fauxhue("LIFX", u, p, None, 0, action_handler = lifx_api_handler())
bulbstate = lazylights.get_state(bulbs)
for bulb in bulbstate:
name = str(bulb.label)
name = name.rstrip('\x00')
lifx.add_bulb(name, state=bool(bulb.power), brightness=(255 * bulb.brightness / 255), private=bulb)

dbg("Entering main loop\n")

while True:
Expand Down