kk
Gabriel Morell





Aethers Home Automation

A series of loosely coupled home automation projects. Components: - Lighting Server with pluggable backends (Lambent Aether) - Controlled for MPower Devices (Ubiquitous Aether) - Centralized Panel for all Aethers (Continuous Aether)

Making it Ubiquitous
posted May 20, 2016, 12:41 a.m.
under aether · perma

Ubiquiti has a line of network enabled power outlets, With nice looking hardware (that won't burn down my house) that comes with a centralized controller and phone applications. This however, didn't quite fit into my vision of a single set of interconnected services that managed all other devices on the network within a single user interface. So I read a few blog posts about accessing the firmware and implemented a small twisted service that exposed a similar API to the Lambent Aether components I had written previously.

This improved on the stock interfaces in a few ways.

  • Outlet names are stored on the service and not the individual client applications.
  • Can manage multiple different types of outlets via a normalized interface.

There was a major fun caveat along the way however!!

The device expires cookies after a few weeks so we have to catch that the cookie is expired and then renew it as part of the control message.

1
2
3
4
5
try:
    requests.put("http://%s/sensors/%i" % (self.host, port), cookies=self.cookie, data={"output": target})
except requests.exceptions.TooManyRedirects:
    self.login() # our mfi cookie was expiring, re-login and then retry
    requests.put("http://%s/sensors/%i" % (self.host, port), cookies=self.cookie, data={"output": target})

I'll one day change that code to use treq and not block, but its fine for now.

Lowering the price w/ ESP8266
posted Sept. 27, 2015, 8 a.m.
under aether · perma

After the fourth install I realized I would run out of budget space fairly quickly if I kept on using a raspberry PI and its assorted accessories for each install. I found a bin that could be flashed to an esp8266 and recieve udp packets for color data on a ws2812 strand; Perfect.

It came with a small C binary with example code to write simple patterns over UDP which while I could call wholesale from twisted, would get hairy fairly quickly. After a quick read I put together an output plugin converted my internal array format into buffers to be sent to the esp8266.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class ESPDevice(BaseDevice):
    def __init__(self, addrs=[], port=7777):
        self.addr = addrs
        self.port = port
        self.socket = socket.socket(socket.AF_INET,  socket.SOCK_DGRAM) # UDP
    def write(self, values):
        chunked = chunks(values, 3)
        # convert list from RGB to GRB internally
        # yay code reuse
        filt = RGBtoGRBLambentOutputFilter()
        filtered = [filt.do_filter(i) for i in chunked]
        values = list(itertools.chain.from_iterable(filtered))
        structd = struct.pack('B'*len(values), *values)
        for a in self.addr:
            self.socket.sendto(structd, (a, self.port))

This output module is instantiated with a list of IP addresses that are injected into the service from the environment, and supports writing UDP packets to multiple devices. This is useful for tileable patterns that may need to be repeated or more realistically the area behind my speaker grilles.

At this time my fixed cost-per install has gone down from $50+LEDs+Power to $3+LEDs+Power. Which means I can expand rapidly.

Running Automated Deploys
posted Aug. 8, 2015, 6 p.m.
under aether · perma

While logging into each host controller box was fun. I soon realized that it would become unwieldy so I put together a few ansible scripts. These scripts download the latest software, configure the service and write configurations for the lights.

Discovering New Instances
posted July 28, 2015, 4 p.m.
under aether · perma

In order to simply deploy of new Lambent devices, I decided to bake in a zeroconf (avahi) announce so that future clients could find all available instances. This worked great for the prototype kivy application being built in parallel and multiple instances could be controlled from a single interface.

Notes on my implementation

Because I was uncertain of how the interfaces on the host machine were configured. I bound the zeroconf announce to every available interface except virtual and the loopback. This ensured I would have working announced on every interface, be it wifi or ethernet.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def announce(self, discovery_name, port=8680):
    self.zeroconf = Zeroconf()

    self.zconfigs = []
    for i in netifaces.interfaces():
        if i.startswith("lo"):
            # remove loopback from announce
            continue
        if i.startswith("veth"):
            # remove docker interface from announce
            continue

        addrs = netifaces.ifaddresses(i)
        if addrs.keys() == [17]:
            continue

        for a in addrs[netifaces.AF_INET]:

            info_desc = {'path': '/progs_grp/', 'name': discovery_name, "port":port}
            config = ServiceInfo("_aether._tcp.local.",
                           "%s_%s_%s_lambent._aether._tcp.local." % (socket.gethostname(),i, port),
                           socket.inet_aton(a['addr']), port, 0, 0,
                           info_desc)
            try:
                self.zeroconf.register_service(config)
                self.zconfigs.append(config)
            except:
                print("Service %s failed to register" % config)
Filtering the output
posted June 20, 2015, 1 p.m.
under aether · perma

So now that I had a base twisted project, a working loop, and some rudimentary lighting programs, I decided to look into output filtering,

In my mind output filters can do any number of mathematical operation to the calculated output. Things like inverting the colors, reordering the output channels. Output filtering is done at the end as part of the write() function before the arrays are converted into bytes and shoved into the output device.

The entire output buffer is passed into a class and the do_filter() function is called. One of the simplest filters written is the RGB to GRB which I used for a ws2811 string that had the channels inverted:

1
2
3
4
5
6
class RGBtoGRBLambentOutputFilter(object):
    _desc = "RGB -> GRB"

    def do_filter(self, rgbvals):
        val = [rgbvals[1], rgbvals[0], rgbvals[2]]
        return val

Based on this I wrote a few useful filters including, inverted colors, pastels, saturated, RGB=>GRB conversion. I also considered output filters that only modify a few pixels in the array, such as a weather overlay filter, and time until next train filter.

Building the Aether
posted April 12, 2015, 4 p.m.
under aether · perma

I spent this past saturday learning twisted and going through the tutorial.

It took me some time to understand initially how twisted worked and that after reading the section on the reactor in the documentation, especially the phrase "You don't call Twisted, Twisted calls you." it finally made sense.

I made a prototype, simplified version of the lighting classes I had written prior and had been running manually via the shell. After confirming that it worked like I iterated and ported my lighting class to support being run via twisted.

The result was a simple lighting server that could be called over telnet, and could switch between the different lighting classes via telnet commands.

The next steps?

  • Web RPC
  • Frontend for Web