Thursday, April 14, 2016

(#22) Amazon's Internet of Things Play 

Navigating the waters (avoiding the piranhas)


I've been making half-hearted attempts at some Cloud players in the IoT space. The two worth a blogpost are Microsoft's and Amazon's.  I'll hold off on Microsoft Azure for awhile, but suffice to say after a few days in URL-hell, after fighting their makefiles, I bailed.

I'll come back to Azure, but I couldn't see any reason to continue the fight. They had their expectations and those weren't mine.  Also 30 days of a "probably" free trial run on Azure wasn't anywhere close to sufficient. So I said "See ya' later -- maybe." to Azure.

Enter Amazon's AWS IoT play.  Well documented, free, an SDK, free, a port to the Raspberry Pi and free.  Did I mention it's free?  Twelve months of free?  And this is not free as in free beer, but free as in beer that costs $0.00.

While well documented, I bumped my head a few times.  Perhaps this post will help you avoid whacking your head on the same obstacles.

Recapping my setup

  1. I'm predominately a consumer of IoT data. I'm not a maker -- I produce IoT events (as MQTT messages) under duress. I only create events because I need them to satisfy my needs for consuming them.
  2. As a consumer, I'll probably take a number of shortcuts (aka hacks) in these posts on AWS IoT. Again, I'm trying to get my equipment to send IoT events as quickly and as painlessly as possible.
  3. I'm developing on Ubuntu 14.04 64bit, and as of this post, it's April 2016.
  4. I develop typically in C, Java and Python for IoT projects

Getting Started

Like everyone else, start with Amazon's IoT Developer's Guide. But pay attention to these details (that I glossed over).

Do not install the AWS CLI Debian package, instead use PIP to get the latest:


$ sudo apt-get install python-pip
$ pip install awscli


Make sure the IoT commands are present by trying:


$ aws iot help

Next, make sure your user/role has the proper permissions. To get going quickly, I opened myself up completely and added the AWSIoTFullAccess policy. Use the AWS Console and IAM to attach a policy:



Follow the developer guide to create a thing. Note that the DG glosses over the Thing's attributes.  Reading ahead, it looks like these attributes (three and at most three?) associated with the Thing are additonal identifiers of the Thing itself.

For example, three attributes could be: the vendor, the version and the serial number of the Thing.  The documentation lists this as an example:


$ aws iot describe-thing --thing-name "MyDevice3"
{
    "thingName": " MyDevice3",
    "defaultClientId": "MyDevice3",
    "attributes": {
        "Manufacturer": "Amazon",
        "Type": "IoT Device A",
        "Serial Number": "10293847562912"
     }

}
So, for my Weather Station Thing I could do something like:

$ aws iot describe-thing --thing-name "WS2308"
{
    "thingName": "WS2308",
    "defaultClientId": "WS2308",
    "attributes": {
        "Manufacturer": "LaCrosse",
        "Type": "WS2308-AL",
        "Serial Number": "00000001"
     }
} 


But setting the Thing Attributes aren't necessary to make progress. So we move on to creating the X.509 Certificates.

AWS IoT X.509 Cert Creation

I hit a couple  of bumps in the documentation, but probably because of my gross unfamiliarity with certificates.  Said another way, you're probably smarter and won't have the same issues.

Nota Bene #1 would be to choose more descriptive certificate file names  in the first create command. The DG shows this:

$ aws iot create-keys-and-certificate --set-as-active --certificate-pem-outfile cert.pem --public-key-outfile publicKey.pem --private-key-outfile privateKey.pem


 
Three files will be created:
1. cert.pem - which will be referred to as the "device certificate" by other documentation and the sample code.  Name the file "cert.pem" if you wish, or name it "myThingCert.pem".
2. The public key cert, as of this point in my exploration, goes unused.
3. The private key cert is properly named, privateKey.pem, and you'll need this one later

Don't Forget the Root Cert

Finally note that we'll need a third Certificate to make things work and it's not created like the other two. It's downloaded.  And it's a short, small mention in the guide so don't miss it.  It's in this section of the Developer Guide

First paragraph:
MQTT clients require a root CA certificate to authenticate with AWS IoT. Download the root CA certificate file from root certificate.

Download it now, call it awsIoTRootCert.pem or something equally descriptive.
 
The AWS IoT SDK Sample code will suck in the complete path and filenames for these three certs so plunk them someplace useful.

Make a Note of the Certificate ARN

When the create-keys-and-certificate command completes, there will be some output similar to:

{
    "certificateArn": "arn:aws:iot:us-east-1:634:cert/cd----x-x-x-x-x-x--x-x-x8d153d02801c", 
    "certificatePem": "-----BEGIN CERTIFICATE----- 

 
Make a note of the certificate ARN (shown in red) as you'll need it later.

Don't change the Version on the Policy

Several steps later, you're asked to create a policy file. This JSON formatted file  as a Version attribute. Don't change it. It needs to be set to "2012-10-17" or things won't work.


$ pconroy@pconroy-VirtualBox:~/Dropbox/Amazon/IoT$ more policy.json {
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Action":["iot:*"],
        "Resource": ["*"]
    }]
 


MQTT.fx

The Developer's Guide uses a GUI java based tool called MQTT.fx to test the MQTT connectivity.  I tried the Debian based package first and it wouldn't start, so I downloaded the JAR file and ran it that way.

When it comes to the step of entering the full Certificate paths and filenames, take note that MQTT.fx again uses different terms.



The root certificate filename goes in the "CA File" text box.  The device certificate filename (called 'cert.pem' in the DG) goes in the "Client Certificate File" text box and the private key cert goes in the Client Key File text box.  The fourth text box, Client Key Password, remains blank.

MQTT.fx - Did you get a Connection Error?

I did. I kept getting this one:


2016-03-30 15:21:19,130 ERROR --- MqttFX ClientModel             : Error when connecting
org.eclipse.paho.client.mqttv3.MqttException: Connection lost
       at org.eclipse.paho.client.mqttv3.internal.CommsReceiver.run(CommsReceiver.java:146) ~[org.eclipse.paho.client.mqttv3-1.0.2.jar:?]
       at java.lang.Thread.run(Thread.java:745) [?:1.8.0_77]
Caused by: java.io.EOFException
       at java.io.DataInputStream.readByte(DataInputStream.java:267) ~[?:1.8.0_77]
       at org.eclipse.paho.client.mqttv3.internal.wire.MqttInputStream.readMqttWireMessage(MqttInputStream.java:65) ~[org.eclipse.paho.client.mqttv3-1.0.2.jar:?]
       at org.eclipse.paho.client.mqttv3.internal.CommsReceiver.run(CommsReceiver.java:107) ~[org.eclipse.paho.client.mqttv3-1.0.2.jar:?]
       ... 1 more
2016-03-30 15:21:19,154  INFO --- ScriptsController              : Clear console.
2016-03-30 15:21:19,156 ERROR --- BrokerConnectService           : MqttException: Connection lost


A little Googling showed that I wasn't alone; others were getting the same error. But it wasn't common, it seemed to be rare.  A there wasn't much in the way of guidance on making the error go away.  



After poking at it for a couple of hours, I started over from the Certificate creation steps - and redid everything.  Tried it again and -- it worked. No errors.


I have no cure just a suggestion that if you too hit a Connection Error, just back up and recreate all of the certs, policies and attachments.

 

AWS IoT SDK for C

That's about all it took for me to work through the Quickstart Section of the Developer's Guide.  At this point, I decided to start in on their SDK and port my existing Home Automation Devices over to using their SDK.  That's the topic of my next post. 

 
 

Sunday, February 21, 2016

Why Not Just Call the Company "And"?

Instead of "TCP Connected"?  There's a part of my home automation setup that controls the lights in the house. No, it's not X10. All of my X10 gear is relegated to a bottom drawer in the closet. No, I chose a lighting control product from a company called TCP Connected.

While I applaud their marketing acumen on obtaining such a clever domain, I'd like to ask them to Google some likely search terms. Try "TCP" or "TCP Connect" or better yet, "TCP Connect Protocol" and watch what pops up.

Yes, thank you, I already know about the three-way handshake.

Enough whining.  


I chose their product because, no surprise, it was relatively inexpensive. And because of a, perhaps misinformed, preference for the 6LoWPAN protocol over Zigbee.  Finally, at the time of selection, the protocol - while not published - was open and had been reverse engineered. Which meant I could exert my own control over the lights and not be limited to using their phone app.

Minor aside - it turns out that their protocol was a bit too open. The vendor closed it up a year or so ago, by switching from http to https and requiring apps to obtain a key (UUID) from the lighting gateway. Fortunately, someone with strong desire figured out how to hack into the new protocol.

Bottom line, we're back online. Thank you Kevin (I hope that's right) for your hard work. You saved the investment we made in this product!  And, yes, I love old, stock Mopar stuff too.  From the '68 Barracuda, with a 440 that Dad brought home one summer. To Steve's '72 Roadrunner with a 383 and Cherry Bombs.


Not an Endorsement

Don't take this as a recommendation for this product.  It works, it works reliably, it works well. It was affordable, but isn't the cheapest offering out there. The future seems uncertain too - as of this writing (February 2016) Home Depot has their TCP Connected products all on clearance shelves.  New, competing products are replacing the TCP Connected bulbs.

Lighting control is such a basic tenet of any Home Automaton project. I didn't spend a lot of time looking, but I see a post in comp.home.automation, from 1994 where someone's having problems with their lights.

So it is a given. The ability to turn on, turn off or dim a light.
That's table stakes for Home Automation.


Then why aren't these products flying off the shelves?

Maybe they are; I don't have access to Home Depot's sales.  I doubt it.  The boxes all look a little dusty in my Home Depots.  Whether they're Phillips Hue, GE Link or the new Wifi Bulbs from Osram.

I'm genuinely interested in what others think.

What percentage of Home Automation projects include Lighting Control?
If the answer is "most" then poor Home Automation sales, in general are the cause.

Or?

What?





Friday, October 30, 2015

(#21) Wow what a View

What an Esper View!


The results are in!

I suspected that my EPL, from this post, while functional, wouldn't be ideal. I thought the views that I just plunked in there to get things working would be a problem in the real world. So, I whipped up some sample MQTT events to emulate what would likely happen in the house.

Here's what usually happens, in our event driven home:
  • Door Status events come about every 10 seconds (+/- a second)
  • Weather Status events come about every 30 seconds (+15 seconds, - 5 seconds)
  • Nest Status events come every two minutes


 
So my test data reproduced that pattern with these conditions
  • door starts off as closed
  • door open event seen with 10 seconds of open time
  • door stays open for a total of 120 seconds


Running that EPL, from this post, on the test data produced these results:

CEP/CONCLUSION | 2015-10-29 18:56:08 -0600 | DOOR LEFT OPEN | Front Door | 61 | OFF | FAN OFF | HOME | 46.2 |
CEP/CONCLUSION | 2015-10-29 18:56:18 -0600 | DOOR LEFT OPEN | Front Door | 61 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-29 18:56:18 -0600 | DOOR LEFT OPEN | Front Door | 71 | OFF | FAN OFF | HOME | 46.2 |
CEP/CONCLUSION | 2015-10-29 18:56:29 -0600 | DOOR LEFT OPEN | Front Door | 81 | OFF | FAN OFF | HOME | 46.2 |
CEP/CONCLUSION | 2015-10-29 18:56:39 -0600 | DOOR LEFT OPEN | Front Door | 91 | OFF | FAN OFF | HOME | 46.2 |
CEP/CONCLUSION | 2015-10-29 18:56:49 -0600 | DOOR LEFT OPEN | Front Door | 71 | OFF | FAN OFF | HOME | 46.2 |

The door status events are sent in chronological order – 61, then 71, then 81, then 91. The numbers are the number of seconds the door has been open.

Notice that not only am I getting multiple, duplicate events, the 71 second event trails in at the end. Clearly using a time based view of 3 minutes isn't ideal.

There are probably a few ways to tackle this – let's crack open the Esper Reference documentation and try a few.

Different View Durations

With the door sensors coming in every 10 seconds, weather events every 30 seconds and Nest events every two minutes, what happens if we alter the EPL to account for the differing times?

anEPLQuery = "SELECT * FROM "
+ " HHBStatusEvent.win:time(10 seconds) as doe, "
+ " NestStatusEvent.win:time(2 min) as nse, "
+ " WS2308WeatherStatusEvent.win:time(30 seconds) as wse "
+ "WHERE "
+ " ( (doe.deviceType = 3 " // Open Close sensors are of type 3
+ " AND doe.deviceStatus = 'OPEN' " // Door is open
+ " AND doe.statusDuration > 60 " // Door has been open for more than 60 seconds
+ " AND doe.deviceOnline = TRUE ) " // Sensor is online
+ " AND (nse.awayStatus = 'HOME') " // Nest thinks we're home
+ " AND ( ((wse.oTemp - nse.temperature ) < -10) OR" // the difference more that 10 degrees
+ " ((wse.oTemp - nse.temperature ) > 10) ) )"


And here's the new output:

CEP/CONCLUSION | 2015-10-29 19:34:58 -0600 | DOOR LEFT OPEN | Front Door | 61 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-29 19:35:08 -0600 | DOOR LEFT OPEN | Front Door | 71 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-29 19:35:19 -0600 | DOOR LEFT OPEN | Front Door | 81 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-29 19:35:29 -0600 | DOOR LEFT OPEN | Front Door | 91 | OFF | FAN OFF | HOME | 36.2 |

So this is better – duplicates are eliminated. 

 
But can we gild the gold? Paint the Lily? Throw perfume on the violet?

Can we make it better?

Suppose I only want to be bugged once every minute, not once every ten seconds? 


From the Esper manual:

With an output clause, the engine dispatches to listeners when the output condition occurs. Here, the output condition is a 1-second time interval. The engine thus outputs every 1 second, starting from the first event, even if there are no new events or no expiring events to output.

Let's add it and see what happens:

anEPLQuery = "SELECT * FROM "
+ " HHBStatusEvent.win:time(10 seconds) as doe, "
+ " NestStatusEvent.win:time(2 min) as nse, "
+ " WS2308WeatherStatusEvent.win:time(30 seconds) as wse "
+ "WHERE "
+ " ( (doe.deviceType = 3 " // Open Close sensors are of type 3
+ " AND doe.deviceStatus = 'OPEN' " // Door is open
+ " AND doe.statusDuration > 60 " // Door has been open for more than 60 seconds
+ " AND doe.deviceOnline = TRUE ) " // Sensor is online
+ " AND (nse.awayStatus = 'HOME') " // Nest thinks we're home
+ " AND ( ((wse.oTemp - nse.temperature ) < -10) OR"
+ " ((wse.oTemp - nse.temperature ) > 10) ) ) "
+ " OUTPUT FIRST EVERY 1 MINUTE"


And the results:

CEP/CONCLUSION | 2015-10-29 20:18:32 -0600 | DOOR LEFT OPEN | Front Door | 61 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-29 20:19:33 -0600 | DOOR LEFT OPEN | Front Door | 121 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-29 20:20:34 -0600 | DOOR LEFT OPEN | Front Door | 181 | OFF | FAN OFF | HOME | 36.2 | 
 
Well that works much better! That's the behavior we're after.






But, why stop now?

Let's go back to the Esper reference manual and dig more into some of the supported view types. 


One of the view types is “length”. 


From the manual:

13.3.1. Length window (win:length)

This view is a moving (sliding) length window extending the specified number of elements into the past. The view takes a single expression as a parameter providing a numeric size value that defines the window size:

If you think about it – regardless of the timing of the event arrival, all we are really interested in are the last matching Door Open event, the last matching Weather Status event and the last matching Nest Status event. Let's change the EPL to this, and try it:

anEPLQuery = "SELECT * FROM "
+ " HHBStatusEvent.win:length( 1 ) as doe, "
+ " NestStatusEvent.win:length( 1 ) as nse, "
+ " WS2308WeatherStatusEvent.win:length( 1 ) as wse "
+ "WHERE "
+ " ( (doe.deviceType = 3 " // Open Close sensors are of type 3
+ " AND doe.deviceStatus = 'OPEN' " // Door is open
+ " AND doe.statusDuration > 60 " // Door has been open for more than 60 seconds
+ " AND doe.deviceOnline = TRUE ) " // Sensor is online
+ " AND (nse.awayStatus = 'HOME') " // Nest thinks we're home
+ " AND ( ((wse.oTemp - nse.temperature ) < -10) OR" // the difference between inside and outside is more that 10 degrees
+ " ((wse.oTemp - nse.temperature ) > 10) ) ) "
// + " OUTPUT FIRST EVERY 1 MINUTE"
;

And the results:

CEP/CONCLUSION | 6:31 -0600 | DOOR LEFT OPEN | Front Door | 61 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 6:41 -0600 | DOOR LEFT OPEN | Front Door | 61 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 6:41 -0600 | DOOR LEFT OPEN | Front Door | 71 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 6:51 -0600 | DOOR LEFT OPEN | Front Door | 81 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 7:01 -0600 | DOOR LEFT OPEN | Front Door | 91 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 7:11 -0600 | DOOR LEFT OPEN | Front Door | 91 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 7:11 -0600 | DOOR LEFT OPEN | Front Door | 91 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 7:11 -0600 | DOOR LEFT OPEN | Front Door | 101 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 7:22 -0600 | DOOR LEFT OPEN | Front Door | 111 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 7:32 -0600 | DOOR LEFT OPEN | Front Door | 121 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 7:42 -0600 | DOOR LEFT OPEN | Front Door | 121 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 7:42 -0600 | DOOR LEFT OPEN | Front Door | 131 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 7:52 -0600 | DOOR LEFT OPEN | Front Door | 141 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 8:02 -0600 | DOOR LEFT OPEN | Front Door | 151 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 8:12 -0600 | DOOR LEFT OPEN | Front Door | 151 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 8:12 -0600 | DOOR LEFT OPEN | Front Door | 161 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 8:22 -0600 | DOOR LEFT OPEN | Front Door | 171 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 8:33 -0600 | DOOR LEFT OPEN | Front Door | 181 | OFF | FAN OFF | HOME | 36.2 |

Hmm – not what we wanted – duplicates aplenty. Not sure why.



What it we added a SELECT DISTINCT to that EPL? 

Instead of “SELECT * FROM”, we change it to “SELECT DISTINCT * FROM”.


In order for an event to be distinct (not a duplicate) every attribute on that event must match another event.



Here's the new EPL:
 
anEPLQuery = "SELECT DISTINCT * FROM "
+ " HHBStatusEvent.win:length( 1 ) as doe, "
+ " NestStatusEvent.win:length( 1 ) as nse, "
+ " WS2308WeatherStatusEvent.win:length( 1 ) as wse "
+ "WHERE "
+ " ( (doe.deviceType = 3 " // Open Close sensors are of type 3
+ " AND doe.deviceStatus = 'OPEN' " // Door is open
+ " AND doe.statusDuration > 60 " // Door has been open for more than 60 seconds
+ " AND doe.deviceOnline = TRUE ) " // Sensor is online
+ " AND (nse.awayStatus = 'HOME') " // Nest thinks we're home
+ " AND ( ((wse.oTemp - nse.temperature ) < -10) OR" // the difference between inside and outside is more that 10 degrees
+ " ((wse.oTemp - nse.temperature ) > 10) ) ) "
// + " OUTPUT FIRST EVERY 1 MINUTE"
;

And the new results!

CEP/CONCLUSION | 2015-10-30 20:48:55 -0600 | DOOR LEFT OPEN | Front Door | 61 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:49:06 -0600 | DOOR LEFT OPEN | Front Door | 61 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:49:06 -0600 | DOOR LEFT OPEN | Front Door | 71 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:49:16 -0600 | DOOR LEFT OPEN | Front Door | 81 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:49:26 -0600 | DOOR LEFT OPEN | Front Door | 91 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:49:36 -0600 | DOOR LEFT OPEN | Front Door | 91 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:49:36 -0600 | DOOR LEFT OPEN | Front Door | 91 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:49:36 -0600 | DOOR LEFT OPEN | Front Door | 101 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:49:46 -0600 | DOOR LEFT OPEN | Front Door | 111 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:49:56 -0600 | DOOR LEFT OPEN | Front Door | 121 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:50:07 -0600 | DOOR LEFT OPEN | Front Door | 121 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:50:07 -0600 | DOOR LEFT OPEN | Front Door | 131 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:50:17 -0600 | DOOR LEFT OPEN | Front Door | 141 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:50:27 -0600 | DOOR LEFT OPEN | Front Door | 151 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:50:37 -0600 | DOOR LEFT OPEN | Front Door | 151 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:50:37 -0600 | DOOR LEFT OPEN | Front Door | 161 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:50:47 -0600 | DOOR LEFT OPEN | Front Door | 171 | OFF | FAN OFF | HOME | 36.2 |
CEP/CONCLUSION | 2015-10-30 20:50:57 -0600 | DOOR LEFT OPEN | Front Door | 181 | OFF | FAN OFF | HOME | 36.2 |




No change. 
I'm not sure why – I'll have to think about that for awhile.




There are scores of other view types/functions in the manual. 

Over time, I'll play with them. But for now, I'll go back to the EPL that combined different view durations and limited the output. 


That's producing the best output, for now.




Wednesday, October 28, 2015

(#20) Mon Dieu, Part Deux!


A good sequel always recaps how we got here. 

So, we've got a house. We've got sensors in the house. We're trying to be a bit smarter about warning us when the door's been open for a long time. Sometimes we want it left open, sometimes we don't. And, finally, I'm confident that I was not born in a barn.

In this post, I said I could think of two ways to detect if a door's been open too long and I want to know about it.  Here was the first way:

If the door has been open for longer than one minute and the furnace/AC is running then send an alert.  

And here's the other way I might want to figure it out.


IF #2


If the door has been open for longer than one minute and the difference between the inside and outside temperatures is large, then send an alert.

So, in this one, I don't have to wait for the furnace or AC to kick on - I'm going to try to respond before that happens.  If the door is open and it's freezing outside, then I want to know. If the door is open and it's Dante's Seventh Level outside, then I want to know.

While this may appear as overly convoluted, it's also helping me learn more about Esper.




The EPL

In my house the outside temperature comes from the Weather Station, and the first floor inside temperature comes from the Nest Thermostat.  So I've got to join three event streams.

String anEPLQuery = "SELECT * FROM "
+ " HHBStatusEvent.win:time(3 min) as doe, "

+ " NestStatusEvent.win:time(3 min) as nse, "
+ " WS2308WeatherStatusEvent.win:time(3 min) as wse "
+ "WHERE "
+ " ( (doe.deviceType = 3 "              // Open Close sensors are of type 3
+ " AND doe.deviceStatus = 'OPEN' "      // Door is open
+ " AND doe.statusDuration > 60 "        // for more than 60 seconds
+ " AND doe.deviceOnline = TRUE ) "      // Sensor is online
+ " AND (nse.awayStatus = 'HOME') "      // Nest thinks we're home


// and the difference between inside and outside is more that 10 degrees 

+ " AND (((wse.oTemp - nse.temperature ) < -10) OR"     
+ "      ((wse.oTemp - nse.temperature ) > 10) ) )";



 

The changes from our first solution are in red.

We're joining a third event stream, status events from our weather station.  We're checking to make sure that Nest thinks we're Home (don't ask why) and we're comparing the inside and outside temperatures.

Anything more that an 10 degree (F) difference triggers the result. (I didn't see an Absolute Value intrinsic function in EPL. I did see a post about invoking Java's Math.Abs function but also a note that it was having an issue. So a simple pair of subtractions seemed easiest.


And to cut to the chase - this works too.

So let's recap what else we need to do to get things moving.  Jump back to this post on Esper if you need a quick refresher on what it takes to get Esper up and processing events.


The Listener

In Esper parlance, a Listener is the class that gets invoked when a match is found; when the EPL finds a set of events that match what you're looking for.

In our EPL, we've asked Esper to send us the Events (as POJOs) that triggered the match (that's what SELECT * does).

So step one on a Listener is to have it implement the UpdateListener interface and override the update method. Like this:

public class LeftDoorOpenListener implements UpdateListener
{
    @Override
    public void update(EventBean[] newData, EventBean[] oldData)
    {

...





      
The update method will get called when a stream of matching events is detected. So, as a first step, let's extract the events that triggered the update:

NestStatusEvent nse = (NestStatusEvent) newData[ 0 ].get( "nse" );
 

HHBStatusEvent hse = (HHBStatusEvent) newData[ 0 ].get( "doe" );

WS2308WeatherStatusEvent  

    wse = (WS2308WeatherStatusEvent) newData[ 0 ].get( "wse" );

We pull the events out using the names we gave them in the EPL statement above.

Next - well, next is up to you.  You've got the three events that triggered the EPL so you can do whatever you'd like to do.  You can do additional processing or checking or throttling in the Listener.




In my case, since I'm an event driven house, I create a new event and plunk it back onto the MQTT bus:

StringBuffer    sb = new StringBuffer();
sb.append( "CEP/CONCLUSION" );              sb.append( " | " );
sb.append( getCurrentDateTime() );          sb.append( " | " );
sb.append( "DOOR LEFT OPEN" );              sb.append( " | " );
sb.append( hse.getDeviceName() );           sb.append( " | " );
sb.append( hse.getStatusDuration() );       sb.append( " | " );
sb.append( nse.getHvacStatus() );           sb.append( " | " );
sb.append( nse.getFanStatus() );            sb.append( " | " );
sb.append( nse.getAwayStatus() );           sb.append( " | " );
sb.append( wse.getoTemp() );                sb.append( " | " );



Which will result in a new MQTT event like this:

CEP/CONCLUSION | 2015-10-28 18:52:10 -0600 | DOOR LEFT OPEN | Front Door | 81 | HEATING | FAN ON | HOME | 49.0 | 

Breaking it down:
MQTT Topic:          CEP/CONCLUSION
Date/Time:           2015-10-28 18:52:10 -0600
Message:             DOOR LEFT OPEN
Device Name:         Front Door
Num Seconds:         81
Furnace/AC:          HEATING
Fan is:              FAN ON 
Home or Away:        HOME
Outside temperature: 49.0


Next up - let's create some more test data and try out a few scenarios.  I think the EPL will need tweaking of the views.

(#19) "What? Were you born in a barn?"


No, Dad. Well, come to think of it, I'm not entirely sure, but I believe I was born in a hospital. But then you'd know better than I.

The point is there are some times when it's fine to leave the door open.

Let's say, hypothetically speaking of course, that you leave last night's pizza in the toaster oven too long and the smoke cloud clashes with the off-white of the walls.  

Or you accidentally microwaved the key fob to the Honda Pilot, again hypothetically speaking, of course and the house smells of burnt plastic.


That's my gripe with simplistic home monitoring. They're more than willing to alert you to a door being left open whether you want it open or not.  So, how could you make a system a bit smarter? Under what conditions would it be OK for the door to be left open?  Or conversely, or what conditions should the door be closed?


Here's an easy one - how about not leaving the door open in the winter?  Nothing aggravates a parent more than using your home's furnace to heat the entire neighborhood.  


And, respectively, keeping the door closed in the summer. Nothing aggravates a parent -- and a climate scientist -- more than using your air conditioner to combat global warming.



Let's go to the whiteboard!

If you've been following along, first you have my sympathies. You really need to spend less time on the Internet.  But, if you've been following along, you know that we have some simple sensors in the house:
  • sensors that tell us if a door is open or closed - and how long it's been that way
  • sensors that tell us the weather outside
  • sensors that tell us the temperature inside
  • sensors that tell us if the furnace is heating or the air conditioner is cooling
  • an MQTT hub that servers up sensor data as events
And we've been toying with a Complex Event Processing engine, called Esper, to see if we could correlate data from these sensors and make conclusions.

All we need to do is figure out how we want to correlate the sensor data to draw a conclusion. In this case, how can we tell if the door is open when it shouldn't be?

Two approaches pop up quickly.


IFTATATTEIF... #1

"If this and this and this then that else if..."  OK- here's one possibility: If the door has been open for longer than one minute and the furnace is running then send an alert.   That would work for the winter.  In the summer it would change slightly: If the door has been open for longer than one minute and the AC is running then send an alert.

This seems straightforward to express in Esper's EPL syntax. Here's our first attempt:

//
// If we get a HHB status event for a Door (type 3) 

// and it's open, and the Furnace is running
//

String anEPLQuery = "SELECT * FROM "
 + "  HHBStatusEvent as doe, NestStatusEvent as nse "
 + "WHERE "
 + " ( (doe.deviceType = 3 "             // Open Close sensors are of type 3
 + " AND doe.deviceStatus = 'OPEN' "     // Door is open
 + " AND doe.statusDuration > 60) "      // for more than 60 seconds
 + " AND (nse.hvacStatus = 'HEATING' )"  // And the Furnace is on



The Door Open Event ('doe') will come in as an HHBStatusEvent object, and the Furnace running event will be a NestStatusEvent ('nse').

It should be readable, you should be able to get the gist.  There are two small problems. First, it won't run-- it's invalid EPL and Esper will barf and second, it doesn't handle the air conditioner running in the summer.

Let's fix the EPL first.

For the 37th time, I'll profess my SQL skills are abysmal and that makes EPL a challenge too. But what we're doing in the EPL above is an "inner join". We're joining two event streams. Esper needs more information before it can successfully join the two event streams.

Here's part of the error message you'll get from Esper (v5.3) when the Esper engine tries to digest that EPL:

Error starting statement: Joins require that at least one view is specified for each stream, no view was specified for doe



What I think Esper is saying is "Look, I need some bounds around the join.In this case, how many events do I hang on to to find the match?  How many results do you want?" 

 
If you think about it - suppose the matching events (door left open and furnace running) happened at 9:00am. Should Esper still call it a match at 5:00pm? 


No, of course not.





There are many ways to specify a view, but the one I'll try is a time limit. If the event match happens at 9:00am, then I'd like the matching events forgotten about after a few minutes.  While this might not be the best way, it's one way and it popped into my head first.  


Let's try this:

String anEPLQuery = "SELECT * FROM "
 + "  HHBStatusEvent.win:time(3 min) as doe, "    // 3 minutes of events

 + "  NestStatusEvent.win:time(3 min) as nse "    // only. Dump anything older
 + "WHERE "
 + " ( (doe.deviceType = 3 "             // Open Close sensors are of type 3
 + " AND doe.deviceStatus = 'OPEN' "     // Door is open
 + " AND doe.statusDuration > 60 ) "     // for more than 60 seconds
 + " AND (nse.hvacStatus = 'HEATING'  )" // And the Furnace is on


What you'll find out is that this compiles, run and works!  It may not be ideal, it may over alarm, over alert - but it does work.  



Now let's fix the Summer issue:

String  anEPLQuery = "SELECT * FROM "
+ "  HHBStatusEvent.win:time(3 min) as doe, "

+ "  NestStatusEvent.win:time(3 min) as nse "
+ "WHERE "
+ " ( (doe.deviceType = 3 "                 // Open Close sensors are of type 3
+ " AND doe.deviceStatus = 'OPEN' "         // Door is open
+ " AND doe.statusDuration > 60 "           // for more than 60 seconds
+ " AND doe.deviceOnline = TRUE ) "         // Sensor is online
+ " AND (nse.hvacStatus in ('HEATING','COOLING') ) )" // Furnace/AC is on


With the changes in red. I added a check to make sure the sensor is actually online. I've noticed that if an HHB sensor is offline, I cannot trust the status to be correct.


And when I run this EPL on some test data, it works! Again - it might not be optimal and I haven't verified the views are the best, but it is working. It finds those events when the door has been left open at the HVAC is running.



I'll skip the other Esper details - the Listener code because I think this post has gone on far enough and made the point I was shooting for:

A house full of sophisticated sensors that act in isolation isn't enough. In fact, it'll annoy the crap out of you!

It's only useful when you're able to draw meaningful conclusions from the wide variety of sensors in the house.


 
Let's save approach #2 for the next post.