Certified Alexa Smart Home Skill
https://skills-store.amazon.com/deeplink/dp/B079VSSQH5?deviceType=app&share&refSuffix=ss_copy
This project came about from a couple of reasons
- Fix a problem that I have with the Nest Thermostat
- Get my 10 year old interested in Computer Science
The issue with Nest is as follows. My single thermostat is located in a part of the house that is far colder than the main living spaces. Unfortunately, Nest does not natively support extra sensors to supply temperature information. The result is the living spaces rising to 25+ degrees celsius, but the Nest was still reading ~21. Moving the Nest to the living room was not a possibility. From research, I was not able to find any 3rd party sensors that would help Nest control the temperature.
For my 10 year old, the possibility of creating something novel was exciting. So was the cash prize of the Alexa Arduino competition =). Whatever gets them excited in technology right?!
nestX ArchitectureThe main challenge of this project was to create an Alexa skill that would talk to a Device Cloud (digital representation of my Arduino thermostat) over OAuth2.0. I was unable to find a provider on the market so created my own Delads Device Cloud (DDC) at http://things.delads.com
The second challenge was to decide on a messaging service that allowed my Arduino communicate with my device cloud. I landed on an MQTT broker that handles the publishing and subscribing of messages. My Arduino thermostat both publishes the current temperature to DDC, but also listens for the Max Temperature value from the user (the value at which the thermostat should turn off). I used a beautiful service from http://shiftr.io who give an awesome visual of messages going across the wire.
Thirdly, since we are eventually controlling a 3rd party thermostat (Nest), we needed a way to send messages over a standard interface. IFTTT both provides the ability to listen to webhooks (sent from DDC) and send commands to Nest. In this case, IF _webhook received_ THEN _set temperature_ ON NEST.
nestX ComponentsDelads Device Cloud (DDC)
A device cloud is a digital representation of your hardware device. It's a dashboard that allows you display sensor values or control your switches. I evaluated a number of device clouds such as https://mydevices.com and https://io.adafruit.com . However, the Alexa Smart Home Skill Kit requires the ability to link a skill to a device cloud over OAuth2.0. This allows Alexa authenticate with the device cloud API when sending and receiving messages. I was unable to find a device cloud in the market that provided this functionality, so I created my own device cloud at http://things.delads.com (Delads Device Cloud, DDC) . All code available at https://github.com/delads/things.
This is essentially a Ruby on Rails application with login functionality. I used bcrypt https://www.npmjs.com/package/bcrypt for password hashing and doorkeeper https://github.com/doorkeeper-gem/doorkeeper for OAuth authentication.
On booting up of the rails application I create a threaded MQTT client for each Arduino Thing that is registered with the platform. I also need to publish changes coming from Alexa to the MQTT broker. I used ruby-mqtt https://github.com/njh/ruby-mqtt as the gem for this.
DDC has an OAuth2.0 authenticated API that allows Alexa communicate with it. Alexa can request the current temperature of a registered device but also send requests such as setTargetTemperature (max temperature at which the thermostat should issue the TURN_OFF command). This is the main API interface between Arduino and the Alexa Home Skills Kit.
MQTT Broker
MQTT is a lightweight messaging service (made popular by the Facebook Messaging App) that is starting to power the IOT movement. It allows Things to publish and listen for messages on specific _topics_ (or queues). This is the main communication layer for nestX.
I evaluated a number of MQTT services such as Adafruit and Mosquitto. I choose http://shiftr.io due to simplicity and their beautiful visuals of messages flowing between connected devices (see green images above). It also works seamlessly with the Arduino MQTT client library from https://github.com/256dpi/arduino-mqtt. They do have some webhook facilities (sending an http request when a message is received on a particular topic) but it's in alpha and didn't work for me. Kudos to the Joel at shiftr for creating an awesome MQTT broker and Arduino client library.
TinyCircuits Arduino shields
I absolutely love these guys! http://tinycircuits.com have created an Arduino processor board the same size as a quarter. You add functionality by adding (stacking) shields with different capabilities. For my project, the standard TinyDuino was not large enough to run an MQTT client so they were kind enough to ship me one of their new prototypes (TinyZero) that allowed me to move forward. The shields used are the WiFi shield, the temperature shield (with the BMP280 sensor as seen on the top board below) and the Display.
Setting up the WiFi board was done as follows
WiFiClient net;
void setup() {
WiFi.setPins(8,2,A3,-1);
WiFi.begin(ssid, pass);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
SerialUSB.print(".");
}
}
The MQTT client from 256dpi https://github.com/256dpi/arduino-mqtt worked well. Setup was as follows, subscribing to a topic called "/max_temp". This is the setTargetTemperature that would have come through from the Alexa.ThermostatController Interface.
#include <MQTTClient.h>
MQTTClient client;
void setup() {
client.begin("broker.shiftr.io",net);
client.onMessage(messageReceived);
connect();
}
void connect() {
SerialUSB.print("connecting...");
while (!client.connect("delads-nestX",SHIFTR_TOKEN,SHIFTR_SECRET )) {
SerialUSB.print(".");
delay(1000);
}
client.subscribe("/max_temp");
}
void loop() {
client.loop();
if(!client.connected()) {
connect();
}
}
void messageReceived(String &topic, String &payload){
SerialUSB.print("Incoming message - topic : " + topic + ", payload : " + payload);
if(topic == "/max_temp"){
maxTemp = payload.toFloat();
SerialUSB.print("Setting maxTemp to " + payload);
}
}
We have logic within the device that if the max_temp _is greater than_ the current temperature from the bmp280 sensor, then we publish a message on the "turn_off_nest" topic. This will be picked up by http://things.delads.com and a trigger will be activated on the IFTTT platform to turn the Nest Off.
double temp = bmp280.readTemperature();
client.publish("/temperature", String(currentTemp));
if(currentTemp > maxTemp){
client.publish("/turn_off_nest", "true");
}
Alexa Smart Home Skill
The interaction model for the Skill was pre-defined by the Smart Home Skill API. If we were creating a custom skill, then this is where we would define the utterances and directives.
The configuration of the skill was the most complicated. This is where you need to provide the authentication information to link the home skill with a specific user on the Device Cloud (in this case, it's http://things.delads.com , or the heroku url since it needs to be over https). More information on account linking for Smart Home can be found here https://developer.amazon.com/docs/custom-skills/link-an-alexa-user-with-a-user-in-your-system.html
Lambda Function (Smart Home Adapter)
Where the Smart Home Skill was mostly configuration, the lambda function (smart home adapter) is where the magic happens. The purpose of the adapter is the following
- Discovery - what type of devices we are exposing to the user
- Report State - current values of a given device
- Set values - Actions you would like to take on a device
DISCOVERY
Understand what type of request is being sent to the function from the Alexa Home Skill. Assume the following User flow. They open the Alexa App, install the nestX skill and go to "Smart Home" on the app. They click on "Add Device" to add their new thermostat from Delads.
This is when the Alexa Home Skill sends an Alexa.Discovery request to your lambda function
if (request.directive.header.namespace === 'Alexa.Discovery' && request.directive.header.name === 'Discover') {
log("DEBUG:", "Discover request", JSON.stringify(request));
handleDiscovery(request, context, "");
}
When returning a response, we tell the Alexa Home Skill that we have a devices of type Alexa.ThermostatController . We make an authenticated http call out to the Delads Device Cloud API to see what devices are available for this user. We describe the following
- Type of device (Alexa.ThermostatController)
- Description of the device (name/manufacturer etc)
- Supported functionality (setting temperature, mode, getting current temperature)
https.get('https://delads-things.herokuapp.com/apis/listthermostats?access_token=' + requestToken, (resp) => {
let data = '';
// A chunk of data has been recieved.
resp.on('data', (chunk) => {
data += chunk;
});
// The whole response has been received.
resp.on('end', () => {
//Let's pull out the maker ID
var json = JSON.parse(String(data));
json.forEach(function(id) {
var thermostatName = id.name;
console.log("DEBUG: HTTP Discover Request : Name = ", thermostatName);
var endpointjson = {
"endpointId": id.id,
"manufacturerName": "Delads",
"friendlyName": id.name,
"description": "Smart Home Device from Delads",
"displayCategories": [
"THERMOSTAT"
],
"cookie": {
},
"capabilities":[
{
"interface": "Alexa.ThermostatController",
"version": "3",
"type": "AlexaInterface",
"properties": {
"supported": [
{
"name": "targetSetpoint"
},
{
"name": "thermostatMode"
}
],
"proactivelyReported": false,
"retrievable": true
}
},
{
"interface": "Alexa.TemperatureSensor",
"version": "3",
"type": "AlexaInterface",
"properties": {
"supported": [
{
"name": "temperature"
}
],
"proactivelyReported": false,
"retrievable": true
}
}
]
}
endpoints.push(endpointjson);
});
var payloadResult = {"endpoints": endpoints};
var responseHeader = request.directive.header;
responseHeader.name = "Discover.Response";
responseHeader.messageId = responseHeader.messageId + "-R";
var response = {
event: {
header: responseHeader,
payload: payloadResult
}
};
console.log("DEBUG: HTTP Discover Request (endpoints) : ", JSON.stringify(response));
context.succeed(response);
});
}).on("error", (err) => {
log("DEBUG:", "HTTP Discover Request","Error: " + err.message);
context.fail("Ohh shite!");
});
REPORT STATE
Once the new devices are registered in the Alexa app, we can then report state on the devices and change values either through the app or through voice on the Alexa Echo devices.
Opening "Kitchen Thermostat" on the Alexa App will call the lambda function with the ReportState namespace.
if (request.directive.header.name === 'ReportState') {
getStateReport(request, context);
}
function getStateReport(request, context) {
var requestToken = request.directive.endpoint.scope.token;
const https = require('https');
https.get('https://delads-things.herokuapp.com/apis/listthermostats?access_token=' + requestToken, (resp) => {
let data = '';
// A chunk of data has been recieved.
resp.on('data', (chunk) => {
data += chunk;
});
// The whole response has been received.
resp.on('end', () => {
//Let's pull out the maker ID
var json = JSON.parse(String(data));
var response;
json.forEach(function(id) {
var deviceId = id.id;
if(deviceId == request.directive.endpoint.endpointId){
var temp = 0;
if(id.temperature != null)
temp = id.temperature;
var contextResult = {
"properties": [
{
"namespace": "Alexa.TemperatureSensor",
"name": "temperature",
"value": {
"value" : temp,
"scale" : "CELSIUS"
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 1000
} ,
{
"namespace":"Alexa.ThermostatController",
"name":"targetSetpoint",
"value":{
"value":id.max_temperature,
"scale":"CELSIUS"
},
"timeOfSample":new Date().toISOString(),
"uncertaintyInMilliseconds":1000
},
{
"namespace": "Alexa.ThermostatController",
"name": "thermostatMode",
"value": "HEAT",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 1000
}
]
};
var endpointResult = {
"scope":{
"type":"BearerToken",
"token": request.directive.endpoint.scope.token,
},
"endpointId":request.directive.endpoint.endpointId,
"cookie":{
}
}
var responseHeader = request.directive.header;
responseHeader.name = "StateReport";
responseHeader.messageId = responseHeader.messageId + "-R";
response = {
context: contextResult,
endpoint: endpointResult,
event: {
header: responseHeader
},
payload: {}
};
} //end if
});
log("DEBUG", "Alexa.ThermostatController ", JSON.stringify(response));
context.succeed(response);
});
}).on("error", (err) => {
log("DEBUG:", "HTTP Discover Request","Error: " + err.message);
context.fail("Ohh shite!");
});
}
SET VALUES
Changing the set temperature on the thermostat either through the app or voice through Alexa Echo will send the SetTargetTemperature request to the lambda function
if (request.directive.header.namespace === 'Alexa.ThermostatController') {
if (request.directive.header.name === 'SetTargetTemperature') {
setTargetTemperature(request, context);
}
}
function setTargetTemperature(request, context) {
var requestMethod = request.directive.header.name;
// get user token pass in request
var requestToken = request.directive.endpoint.scope.token;
if (requestMethod === "SetTargetTemperature") {
var newTemperature = request.directive.payload.targetSetpoint.value;
var thermostat_id = request.directive.endpoint.endpointId;
const https = require('https');
var requestString = 'https://delads-things.herokuapp.com/apis/settemperature?' +
'access_token=' + requestToken +
'&target_temperature=' + newTemperature +
'&id=' + thermostat_id;
log("DEBUG", "Request to Delads ", requestString);
https.get(requestString, (resp) => {
let data = '';
// A chunk of data has been recieved.
resp.on('data', (chunk) => {
data += chunk;
});
// The whole response has been received. Print out the result.
// The response is a JSON representation of the thermostat active record
// Includes the new temperature that the thermostat is set to
resp.on('end', () => {
var json = JSON.parse(String(data));
var contextResult = {
"properties": [{
"namespace": "Alexa.ThermostatController",
"name": "targetSetpoint",
"value": {
"value": json.max_temperature,
"scale": "CELSIUS"
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 500
}, {
"namespace": "Alexa.ThermostatController",
"name": "thermostatMode",
"value": "HEAT",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 500
}]
};
var responseHeader = request.directive.header;
responseHeader.name = "Response";
responseHeader.namespace = "Alexa";
responseHeader.messageId = responseHeader.messageId + "-R";
var response = {
context: contextResult,
event: {
header: responseHeader
},
payload: {}
};
log("DEBUG", "Response from Delads ", json);
log("DEBUG", "Alexa.ThermostatController ", JSON.stringify(response));
context.succeed(response);
});
}).on("error", (err) => {
log("DEBUG:", "HTTP SetTemperature Request","Error: " + err.message);
context.fail("Ohh shite!");
});
}
}
IFTTT
This is the platform that allows our Arduino device to communicate with the Nest thermostat.
STEP 1 - Temperature sensor on our Arduino device registers a value that's greater than the setTargetTemperature (or max temp) set by the user
// On our Arduino device
double temp = bmp280.readTemperature();
if(currentTemp > maxTemp){
client.publish("/turn_off_nest", "true");
}
STEP 2 - On our Rails application (Delads Device Cloud), we have an MQTT Client listening for the "/turn_off_nest" topic
# On startup of our Rails Device Cloud Server
require 'mqtt'
require 'net/https'
require 'uri'
Thread.new do
MQTT::Client.connect('mqtt://xxxxxxx:xxxxx@broker.shiftr.io') do |c|
c.subscribe( 'temperature' )
c.subscribe( 'turn_off_nest')
## Let's send a trigger to IFTTT if max temp is reached
c.get do |topic,message|
if(topic == "turn_off_nest")
uri = URI('https://maker.ifttt.com/trigger/nestX_hit_max_temp/with/key/xxxxxx')
Net::HTTP.start(uri.host, uri.port,
:use_ssl => uri.scheme == 'https') do |http|
request = Net::HTTP::Get.new uri
http.request request # Net::HTTPResponse object
end
end
end
end
end
STEP 3 - Add the Webhooks Applet on IFTTT that sends a message to turn_off Nest.
Comments