Smart Home with HANA, IFTTT and Smartthings
Demo Jam, Smartthings, HANA and IFTTT
For Demo Jam in Las Vegas 2014 a Smart Home scenario was created using Smartthings, HANA, IFTTT and a number of sensors. IoT in the Smart Home offers many possibilities (e.g. Insurance integration), a light hearted approach was taken for the demo with a homework detector created with a Raspberry Pi. When the homework is complete an event allows an XBox to be turned on and an SMS sent to the parents to review the completed homework.
The core of the Smart Home was a Smartthings hub with connected devices. This was augmented with IFTTT to enable the pushing of a plethora of events into the HANA Smartthings device.
Connecting Smartthings to HANA
Smartthings home automation from a user perspective is a mobile experience. Everything user interaction is via a mobile app from configuring new sensors to setting alerts and rules..
The HANA device type with its characteristics:
Smartthings also has a thriving developer community and extensible system. This allows hobbyists and makers to create devices, smartapps and device types for Smartthings.
A new device for HANA was created. By collecting the data from sensors and storing the data in HANA smart decisions can be made. Each device has a number of characteristics. This allowed a HANA device to be created that could combine the states of multiple sensors and provide insights in the mobile application.
Creating a new device type can be achieved in the web IDE for smartthings. The code below was used to create the HANA device type. This device type was a modified Raspberry Pi device from Nicholas Wilde.
/**
* HANASmartHome
*
* Copyright 2014 John Astill
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
import groovy.json.JsonSlurper
preferences {
input("ip", "string", title:"IP Address", description: "21.21.21.21", required: true, displayDuringSetup: true)
input("port", "string", title:"Port", description: "8000", defaultValue: 8000 , required: true, displayDuringSetup: true)
input("username", "string", title:"Username", description: "username", required: true, displayDuringSetup: true)
input("password", "password", title:"Password", description: "Password", required: true, displayDuringSetup: true)
}
metadata {
definition (name: "HANASmartHome", namespace: "jastill", author: "John Astill") {
capability "Polling"
capability "Refresh"
capability "Actuator"
capability "Switch"
capability "Sensor"
capability "Alarm"
capability "Contact Sensor"
attribute "cpuPercentage", "string"
attribute "memory", "string"
attribute "diskUsage", "string"
attribute "readings", "string"
attribute "contact", "string"
attribute "homeCondition", "string"
}
simulator {
// TODO: define status and reply messages here
}
tiles {
standardTile("button", "device.switch", width: 1, height: 1, canChangeIcon: false) {
state "off", label: 'Off', icon: "st.Electronics.electronics18", backgroundColor: "#ffffff", nextState: "on"
state "on", label: 'On', icon: "st.Electronics.electronics18", backgroundColor: "#79b821", nextState: "off"
}
standardTile("homeCondition", "device.homeCondition", width: 1, height: 1, canChangeIcon: false) {
state ("safe", label: 'Safe', icon: "st.Home.home2", backgroundColor: "#79b821", nextState: "unsafe")
state ("unsafe", label: 'Unsafe', icon: "st.Home.home2", backgroundColor: "#bc2323", nextState: "safe")
state ("refresh", label: 'Checking', icon: "st.Home.home2", backgroundColor: "#ffffff", nextState: "refresh")
}
valueTile("temperature", "device.temperature", width: 1, height: 1) {
state "temperature", label:'${currentValue}°', unit: "F",
backgroundColors:[
[value: 25, color: "#153591"],
[value: 35, color: "#1e9cbb"],
[value: 47, color: "#90d2a7"],
[value: 59, color: "#44b621"],
[value: 67, color: "#f1d801"],
[value: 76, color: "#d04e00"],
[value: 77, color: "#bc2323"]
]
}
valueTile("memory", "device.memory", width: 1, height: 1) {
state "default", label:'${currentValue} MB', unit:"MB",
backgroundColors:[
[value: 353, color: "#153591"],
[value: 287, color: "#1e9cbb"],
[value: 210, color: "#90d2a7"],
[value: 133, color: "#44b621"],
[value: 82, color: "#f1d801"],
[value: 26, color: "#d04e00"],
[value: 20, color: "#bc2323"]
]
}
valueTile("diskUsage", "device.diskUsage", width: 1, height: 1) {
state "default", label:'${currentValue}% Disk', unit:"Percent",
backgroundColors:[
[value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
}
valueTile("readings", "device.readings", width: 1, height: 1) {
state "default", label:'${currentValue} Readings', unit:"K",
backgroundColors:[
[value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
}
standardTile("alarm", "device.alarm", inactiveLabel: false, decoration: "flat") {
state "default", action:"alarm", label: "Emergency", displayName: "Emergency"
}
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
state "default", action:"refresh.refresh", icon: "st.secondary.refresh"
}
main "homeCondition" // Main View
details([ "homeCondition", "readings", "temperature","button", "refresh"])
}
}
// parse events into attributes
def parse(String description) {
log.debug "Parsing '${description}'"
def map = [:]
def descMap = parseDescriptionAsMap(description)
def body = new String(descMap["body"].decodeBase64())
def slurper = new JsonSlurper()
def result = slurper.parseText(body)
if (result){
log.debug "Computer is up"
sendEvent(name: "switch", value: "on")
}
if (result.containsKey("homeCondition")) {
log.debug "homeCondition: ${result.homeCondition}"
sendEvent(name: "homeCondition", value: result.homeCondition)
if (result.homeCondition == "unsafe") {
// Send a push somehow
sendEvent(name:contact, value:"open")
}
}
if (result.containsKey("readings")) {
int i = result.readings.toInteger()
log.debug "readings: ${result.readings}"
sendEvent(name: "readings", value: i/1000)
}
if (result.containsKey("cpu_temp")) {
log.debug "temperature: ${result.cpu_temp}"
sendEvent(name: "temperature", value: result.cpu_temp)
}
if (result.containsKey("mem_avail")) {
log.debug "mem_avail: ${result.mem_avail}"
sendEvent(name: "memory", value: result.mem_avail)
}
if (result.containsKey("disk_usage")) {
log.debug "disk_usage: ${result.disk_usage}"
sendEvent(name: "diskUsage", value: result.disk_usage)
}
}
// Commands
def poll() {
log.debug "Executing 'poll'"
sendEvent(name: "switch", value: "off")
sendEvent(name: "homeCondition", value: "refresh")
getHANAData()
}
def refresh() {
log.debug "Executing 'refresh'"
sendEvent(name: "switch", value: "off")
sendEvent(name: "homeCondition", value: "refresh")
getHANAData()
}
private getHANAData() {
def uri = "/iottt/services/getData.xsjs"
postAction(uri)
}
def strobe() {
log.debug "Executing 'strobe'"
}
def siren() {
log.debug "Executing 'siren'"
}
def both() {
log.debug "Executing 'both'"
}
def off() {
log.debug "Executing 'off'"
}
// ------------------------------------------------------------------
private postAction(uri){
setDeviceNetworkId(ip,port)
def userpass = encodeCredentials(username, password)
def headers = getHeader(userpass)
def hubAction = new physicalgraph.device.HubAction(
method: "GET",
path: uri,
headers: headers
)//,delayAction(1000), refresh()]
log.debug("Executing hubAction on " + getHostAddress())
//log.debug hubAction
hubAction
}
// ------------------------------------------------------------------
// Helper methods
// ------------------------------------------------------------------
def parseDescriptionAsMap(description) {
description.split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
}
private encodeCredentials(username, password){
log.debug "Encoding credentials"
def userpassascii = "${username}:${password}"
def userpass = "Basic " + userpassascii.encodeAsBase64().toString()
// log.debug "ASCII credentials are ${userpassascii}"
// log.debug "Credentials are ${userpass}"
return userpass
}
private getHeader(userpass){
log.debug "Getting headers"
def headers = [:]
headers.put("HOST", getHostAddress())
headers.put("Authorization", userpass)
//log.debug "Headers are ${headers}"
return headers
}
private delayAction(long time) {
new physicalgraph.device.HubAction("delay $time")
}
private setDeviceNetworkId(ip,port){
def iphex = convertIPtoHex(ip)
def porthex = convertPortToHex(port)
device.deviceNetworkId = "$iphex:$porthex"
log.debug "Device Network Id set to ${iphex}:${porthex}"
}
private getHostAddress() {
return "${ip}:${port}"
}
private String convertIPtoHex(ipAddress) {
String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join()
return hex
}
private String convertPortToHex(port) {
String hexport = port.toString().format( '%04x', port.toInteger() )
return hexport
}
With this in place we now have HANA acting as a Smartthings device but we need to get data into the database.
Enabling the push to HANA via IFTTT
Pushing data to HANA was implemented as a ‘hack’. IFTTT does not recognize HANA as an endpoint but does have a Smartthings channel that receives events from the devices. Looking at the available channels at the time WordPress was an interesting option. WordPress allows plugins that react to events. By creating a plugin that posted to HANA when post is made to WordPress we enabled IFTTT to integrate with HANA.
For the sake of the demo a full WordPress install was used so that we could visualize the posts in a demo. However there are more lightweight webhook options that could also have been used that would also have been more secure, one example is ifttt-wordpress-webhook which also supports node.js.
IFTTT uses channels to act as the source and sink of events. Here is the configuration of WordPress as a channel. This is a prerequisite before a channel can be used in a Recipe.
Recipes are created with a wizard of 7 steps (at time of writing this). Step 6 is where you can take the characteristics (Ingredients) of the source channel and map them to the sink channel. In this example we know we are working with a switch and take the device name and event time and pass it to WordPress in JSON format.
The complete Recipe is as below, it shows the source event as a switch in Erics room turning on and the sink as the Worpdress blog.
The posts to wordpress look like below. The events below are from a different device that was configured but shows the JSON post that is then parsed by the WordPress plugin.
The WordPress plugin code is relatively straightforward. This is written in PHP and deployed to the wp-content/plugins directory of the WordPress install.
When a save_post event occurs the JSON data is parsed and the values are posted to HANA using a simple OData service.
<?php
/**
* Plugin Name: Demo Jam Hana
* Plugin URI: http://URI_Of_Page_Describing_Plugin_and_Updates
* Description: A brief description of the Plugin.
* Version:0.0.1
* Author: John Astill
* Author URI: http://URI_Of_The_Plugin_Author
* License: A "Slug" license name e.g. GPL2
*/
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
defined('ABSPATH') or die("No script kiddies please!");
add_action ( 'save_post', 'pushToHana' );
function pushToHana( $post_id ) {
global $wpdb;
// Read the post with the given post id
$query = "SELECT * FROM ".$wpdc->prefix."wp_posts WHERE id='$post_id'";
$postings = $wpdb->get_results($query);
// There should be only one
foreach ($postings as $posting) {
$device = "external";
$action = "";
$value = "0";
$uom = "";
$timestamp= "";
$title = utf8_encode($posting->post_title);
$po = json_decode($title,true);
if ($po == NULL) {
var_dump(json_last_error_msg());
var_dump($po);
} else {
if (array_key_exists("device",$po)) {
$device = urlencode($po["device"]);
}
if (array_key_exists("value",$po)) {
$value = urlencode($po["value"]);
}
if (array_key_exists("uom",$po)) {
$uom = urlencode($po["uom"]);
}
if (array_key_exists("action",$po)) {
$action = urlencode($po["action"]);
}
if (array_key_exists("timestamp",$po)) {
$timestamp = urlencode($po["timestamp"]);
}
}
sendToHana( $device, $value, $uom, $action, $timestamp);
}
}
function sendToHana($sid, $value, $uom, $action, $ifttttime) {
$url = "http://user:pass@21.21.21.21:8000/iottt/services/createEntry.xsjs?sid=$sid&value=$value&uom=$uom&action=$action&ifttttime=$ifttttime";
// create a new cURL resource
$ch = curl_init();
// set URL and other appropriate options
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
// grab URL and pass it to the browser
curl_exec($ch);
// close cURL resource, and free up system resources
curl_close($ch);
}
?>
The demo was a lot of fun to build, including an integrated Nerf gun which later was salvaged and added to the IoT Scarecrow. The homework monitor was an inbox with a switch that triggered an event on a Raspberry Pi. Webiopi and SQL Anywhere were running on the Pi to store and distribute the events.
IoT and a Smart Home has so many possibilities and will generate an incredible amount of events. Making sense and taking insights from this data will continue to create opportunities for the foreseeable future.
Hi John,
"Pretty" amazing/cool stuff....
I still remember your DemoJam performance on the SAP TechEd in Madrid when we saw your wife on stage, turning on the washing machine. You've come a long way since then 😆 😉
Grtz and cu in Barcelona?
Sven
Hello John,
Nice work, Your blog routes us to new ideas on IoT Topic.
Thank you
Hi John,
nice work. Really nice to see all the possibilities.
~florian