My lambda code for the skill backend, don’t laugh, my NodeJS isn’t as good as my c# or GoLang. @Colleen this might something you want. Pair this with this write up: Full Alexa Home Automation for the Particle + Skill - Particle Projects
And check this out, though I’m not sure it covers home automation skills: Alexa Developer Tutorials & Code Samples - Alexa Skills Kit Official Site
var https = require(‘https’);
var particleHttpsHost = “api.particle.io”;
var particleHttpsPath = “/v1/devices”
var particleV2Endpoint = “echoDeviceV2”;
var particleV3Endpoint = “echoDeviceV3”;
var particleV2CmdEndPoint = “echoCmdV2”;
var particleV3CmdEndPoint = “echoCmdV3”;
/**
-
Main entry point.
-
Incoming events from Alexa Lighting APIs are processed via this method.
*/
exports.handler = function(event, context) {
log(‘Input’, event);
switch (event.header.namespace) {
/**
* The namespace of "Discovery" indicates a request is being made to the lambda for
* discovering all appliances associated with the customer's appliance cloud account.
* can use the accessToken that is made available as part of the payload to determine
* the customer.
*/
case 'Alexa.ConnectedHome.Discovery':
handleDiscovery(event.payload.accessToken, context);
break;
/**
* The namespace of "Control" indicates a request is being made to us to turn a
* given device on, off or brighten. This message comes with the "appliance"
* parameter which indicates the appliance that needs to be acted on.
*/
case 'Alexa.ConnectedHome.Query':
case 'Alexa.ConnectedHome.Control':
handleControl(event, context);
break;
/**
* We received an unexpected message
*/
default:
log('Err', 'No supported namespace: ' + event.header.namespace);
context.fail('Something went wrong');
break;
}
};
/**
-
This method is invoked when we receive a “Discovery” message from Alexa Connected Home Skill.
-
We are expected to respond back with a list of appliances that we have discovered for a given
-
customer.
*/
function handleDiscovery(accessToken, context) {
getDevices(“alexa”, getDeviceCapabilitiesV2, accessToken, context);
}
/**
-
Control events are processed here.
-
This is called when Alexa requests an action (IE turn off appliance).
*/
function handleControl(event, context) {
console.log(“handleControl Called.”);
//console.log("event: " + JSON.stringify(event, null, 4));
//console.log("context: " + JSON.stringify(context, null, 4));
if (event.header.namespace === ‘Alexa.ConnectedHome.Control’ || event.header.namespace === ‘Alexa.ConnectedHome.Query’) {
/**
* Retrieve the appliance id and accessToken from the incoming message.
*/
var accessToken = event.payload.accessToken;
var particleDeviceId = event.payload.appliance.additionalApplianceDetails.particleDeviceId
var particleDeviceIndex = event.payload.appliance.additionalApplianceDetails.deviceIndex
var message_id = event.header.messageId;
var param = "";
var index = "0";
var state = 0;
var confirmation;
var particleFunctionString = particleDeviceIndex + ":";
//log("Access Token: ", accessToken);
//log("DeviceID: ", particleDeviceId);
//log("Device Index: ", particleDeviceIndex);
log("event ",event.header.name);
var payload = {
achievedState : {}
};
switch (event.header.name)
{
case "TurnOnRequest":
particleFunctionString += "1:1";
confirmation = "TurnOnConfirmation";
break;
case "TurnOffRequest":
particleFunctionString += "1:0";
confirmation = "TurnOffConfirmation";
break;
case "SetPercentageRequest":
particleFunctionString += "2:" + event.payload.percentageState.value;
confirmation = "SetPercentageConfirmation";
break;
case "IncrementPercentageRequest":
particleFunctionString += "2:U" + event.payload.deltaPercentage.value;
confirmation = "IncrementPercentageConfirmation";
break;
case "DecrementPercentageRequest":
particleFunctionString += "2:D" + event.payload.deltaPercentage.value;
confirmation = "DecrementPercentageConfirmation";
break;
case "SetColorRequest":
//console.log("Getting RGB");
var rgb = hsvToRgb(event.payload.color.hue / 360, event.payload.color.saturation, event.payload.color.brightness);
//console.log("RGB: " + JSON.stringify(rgb,null,4));
particleFunctionString += "3:" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ",HSV " + event.payload.color.hue + "," + event.payload.color.saturation + "," + event.payload.color.brightness;
confirmation = "SetColorConfirmation";
payload = {achievedState:{color: event.payload.color}};
break;
case "GetLockStateRequest":
log("Getting Lock Request");
particleFunctionString += "4:1,0";
log("ParticleFunctionString: ", particleFunctionString);
confirmation = "GetLockStateResponse";
break;
case "SetLockStateRequest":
log("Setting Lock Request",event.payload.lockState);
particleFunctionString += "4:0," + ((event.payload.lockState == "LOCKED") ? "1" : "0");
log("ParticleFunctionString: ", particleFunctionString);
confirmation = "SetLockStateConfirmation";
payload = {lockState: event.payload.lockState};
break;
case "GetTemperatureReadingRequest":
particleFunctionString += "5:1,0";
confirmation = "GetTemperatureReadingResponse";
break;
case "GetTargetTemperatureRequest":
particleFunctionString += "5:2,0";
confirmation = "GetTargetTemperatureResponse";
break;
case "SetTargetTemperatureRequest":
particleFunctionString += "5:0," + parseInt(cToF(event.payload.targetTemperature.value));
confirmation = "SetTargetTemperatureConfirmation";
payload = {targetTemperature: {value: parseInt(event.payload.targetTemperature.value)},temperatureMode: {value: "HEAT"}};
break;
case "IncrementTargetTemperatureRequest":
particleFunctionString += "5:0,U" + parseInt(event.payload.deltaTemperature.value);
confirmation = "IncrementTargetTemperatureConfirmation";
break;
case "DecrementTargetTemperatureRequest":
particleFunctionString += "5:0,D" + parseInt(event.payload.deltaTemperature.value);
confirmation = "DecrementTargetTemperatureConfirmation";
break;
case "SetColorTemperatureRequest":
particleFunctionString += "6:" + event.payload.colorTemperature.value;
confirmation = "SetColorTemperatureConfirmation";
break;
case "IncrementColorTemperatureRequest":
particleFunctionString += "6:U100" + event.payload.deltaTemperature.value;
confirmation = "IncrementColorTemperatureConfirmation";
break;
case "DecrementColorTemperatureRequest":
particleFunctionString += "6:D100" + event.payload.colorTemperature.value;
confirmation = "DecrementColorTemperatureConfirmation";
break;
}
var options = {
hostname: particleHttpsHost,
port: 443,
path: particleHttpsPath + "/" + particleDeviceId + "/" + particleV2CmdEndPoint,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
var data = "access_token=" + accessToken + "&" + "args=" + particleFunctionString;
var serverError = function (e) {
log('Error', e.message);
context.fail(generateControlError('TurnOnRequest', 'DEPENDENT_SERVICE_UNAVAILABLE', 'Unable to connect to server'));
};
var callback = function(response) {
var str = '';
response.on('data', function(chunk) {
str += chunk.toString('utf-8');
});
response.on('end', function() {
var particleResponse = JSON.parse(str);
var headers = {
namespace: 'Alexa.ConnectedHome.Control',
name: confirmation,
payloadVersion: '2',
messageId: message_id
};
log('Return Value: ' + JSON.stringify(particleResponse,null,4));
switch (event.header.name)
{
case "GetLockStateRequest":
payload = (particleResponse.return_value == "1") ? {lockState:"LOCKED"} : {lockState:"UNLOCKED"};
headers.namespace = "Alexa.ConnectedHome.Query";
break;
case "GetTargetTemperatureRequest":
case "GetTemperatureReadingRequest":
payload = {"temperatureReading": { value: parseFloat(fToC(particleResponse.return_value)), scale: "FAHRENHEIT" }};
headers.namespace = "Alexa.ConnectedHome.Query";
break;
}
var result = {
header: headers,
payload: payload
};
console.log(result);
context.succeed(result);
});
response.on('error', serverError);
};
var req = https.request(options, callback);
req.on('error', serverError);
req.write(data);
req.end();
}
}
/**
- Utility functions.
*/
function log(title, msg) {
console.log(title + ": " + msg);
}
function generateControlError(name, code, description) {
var headers = {
namespace: ‘Control’,
name: name,
payloadVersion: ‘1’
};
var payload = {
exception: {
code: code,
description: description
}
};
var result = {
header: headers,
payload: payload
};
return result;
}
/*
exports.getJSON = function(options, onResultCallback, callbackParams)
{
//console.log(“rest::getJSON”);
var prot = options.port == 443 ? https : http;
var req = prot.request(options, function(res)
{
var output = '';
//console.log(options.host + ':' + res.statusCode);
res.setEncoding('utf8');
res.on('data', function (chunk) {
output += chunk;
});
req.on('socket', function (socket) {
socket.setTimeout(3000);
socket.on('timeout', function() {
log('Timeout calling URL', JSON.stringify(options,null,4));
var errorObject = {statusCode: 500, error: "TimeOut"};
onResultCallback(500, errorObject, callbackParams);
});
});
res.on('end', function() {
console.log("PreJSON Output: " + output);
var obj = JSON.parse(output);
onResultCallback(res.statusCode, obj, callbackParams);
});
});
req.on('error', function(err) {
log("Error calling URL", JSON.stringify(err,null,4));
console.log('error: ' + err.message);
var errorObject = {statusCode: 500, error: err.message};
onResultCallback(500, errorObject, callbackParams);
});
req.end();
};
*/
exports.getJSON = function(options, onResultCallback, callbackParams)
{
//console.log(“rest::getJSON”);
var http = require('http');
var prot = options.port == 443 ? https : http;
var req = prot.request(options, function(res)
{
var output = '';
//console.log(options.host + ':' + res.statusCode);
res.setEncoding('utf8');
res.on('data', function (chunk) {
output += chunk;
});
res.on('end', function() {
console.log("PreJSON Output: " + output);
var obj = JSON.parse(output);
onResultCallback(res.statusCode, obj, callbackParams);
});
});
req.on('socket', function (socket) {
socket.setTimeout(2000);
socket.on('timeout', function() {
console.log('Timeout calling URL', JSON.stringify(options,null,4));
var errorObject = {statusCode: 504, error: "TimeOut"};
onResultCallback(504, errorObject, callbackParams);
socket.destroy();
});
});
req.on('error', function(err) {
console.log("Error calling URL", JSON.stringify(err,null,4));
console.log('error: ' + err.message);
var errorObject = {statusCode: 500, error: err.message};
onResultCallback(500, errorObject, callbackParams);
});
req.end();
};
var deviceDiscoveryComplete = function(context) {
console.log("Discovery Response: " + JSON.stringify(responseTemplateV2, null, 4));
context.succeed(responseTemplateV2);
}
var serverError = function (e) {console.log(‘Error’, e.message);};
function isBitSet(num, bit){
return ((num>>bit) % 2 != 0)
}
var responseTemplateV2 = {
header:{
name: “DiscoverAppliancesResponse”,
namespace: “Alexa.ConnectedHome.Discovery”,
payloadVersion: “2”
},
payload:{
discoveredAppliances:
}
};
var deviceTemplateV2 = {
applianceId: ‘’,
manufacturerName: ‘Echo Particle Bridge’,
modelName: ‘Particle Bridge V2’,
version: ‘1’,
friendlyName: ‘’,
friendlyDescription: ‘’,
isReachable: true,
actions:[
],
applicanceTypes:[
],
additionalApplianceDetails: {
deviceIndex: 0,
particleDeviceId: “”
}
};
var getDeviceCapabilitiesV2 = function(callback, finalCallback, params) {
console.log("Params: " + JSON.stringify(params,null,4));
var deviceId;
deviceId = params.devices[params.deviceIndex];
var options = {
hostname: particleHttpsHost,
port: 443,
path: particleHttpsPath + "/" + deviceId + "/" + particleV2Endpoint,
method: 'GET',
headers: {
'Authorization' : "Bearer " + params.accessToken
}
};
console.log(JSON.stringify(options,null,4));
var callback = function(status, reply, params) {
console.log("Reply: " + JSON.stringify(reply,null,4));
if (reply.ok == false) {
log("API Errors: ", reply.errors);
}
else if (reply.error) {
log("API Errors: ", reply.error);
}
else
{
//make a copy of the json object
reply = JSON.parse(reply.result.replace("\\\"","\""));
if (reply.error) {
log('Device Capabillities API Error: ',reply.error);
}
else
{
//console.log(JSON.stringify(reply,null,4));
for (var i = 0; i < reply.Devices.length; i++) {
//console.log(i);
console.log("Creating Device Details for: " + reply.Devices[i].name);
//console.log(reply.Devices[i].type);
var device = JSON.parse(JSON.stringify(deviceTemplateV2));
device.applianceId = deviceId + "_" + i;
device.friendlyName = reply.Devices[i].name;
device.friendlyDescription = "Particle IO Bridge";
device.additionalApplianceDetails.deviceIndex = i;
device.additionalApplianceDetails.particleDeviceId = deviceId
//Turn On/Off
if (isBitSet(reply.Devices[i].type,1)) {
device.actions.push("turnOn");
device.actions.push("turnOff");
}
//Percent
if (isBitSet(reply.Devices[i].type,2)) {
device.actions.push("setPercentage")
device.actions.push("decrementPercentage")
device.actions.push("incrementPercentage")
}
//SetColor
if (isBitSet(reply.Devices[i].type,3)) {
device.actions.push("setColor")
}
//Lock
if (isBitSet(reply.Devices[i].type,4)) {
device.actions.push("getLockState")
device.actions.push("setLockState")
device.applicanceTypes.push("SMARTLOCK");
}
//Temperature
if (isBitSet(reply.Devices[i].type,5)) {
device.actions.push("incrementTargetTemperature");
device.actions.push("decrementTargetTemperature");
device.actions.push("setTargetTemperature");
device.actions.push("getTargetTemperature");
device.actions.push("getTemperatureReading");
device.applicanceTypes.push("THERMOSTAT");
}
//Color Temp
if (isBitSet(reply.Devices[i].type,6)) {
device.actions.push("setColorTemperature")
device.actions.push("decrementColorTemperature")
device.actions.push("incrementColorTemperature")
}
responseTemplateV2.payload.discoveredAppliances.push(device);
}
}
}
params.deviceIndex++;
if (params.deviceIndex < params.devices.length)
{
log("Calling getDeviceCapabilitesV2",JSON.stringify(params,null,4));
getDeviceCapabilitiesV2(getDeviceCapabilitiesV2, deviceDiscoveryComplete, params)
}
else
{
deviceDiscoveryComplete(params.context);
}
};
var q = exports.getJSON(options, callback, params);
}
function getDevices(key, deviceProccessingCallback, accessToken, context) {
var options = {
hostname: particleHttpsHost,
port: 443,
path: particleHttpsPath,
method: 'GET',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization' : "Bearer " + accessToken
}
};
var callback = function(status, reply, params) {
console.log("Device Discovery JSON Response: " + JSON.stringify(reply,null,4));
if (reply.ok == false) {
return console.log("API Errors: ", reply.errors);
}
var devices = [];
key = key.toUpperCase();
for (var i = 0; i < reply.length; i++) {
//console.log(i);
//console.log(reply[i].id);
console.log("Reviewing Settings for Device Named: " + reply[i].name);
if (key.length == 0)
{
devices.push(reply[i].id);
}
//console.log(reply[i].name.toUpperCase());
//console.log(reply[i].name.toUpperCase().indexOf(key));
if (reply[i].name != null) {
if (reply[i].connected == true)
{
devices.push(reply[i].id);
//console.log("Found");
}
}
}
console.log("Found Devices: " + JSON.stringify(devices,null,4));
params.devices = devices;
getDeviceCapabilitiesV2(getDeviceCapabilitiesV2, deviceDiscoveryComplete, params)
};
var params = {
"devices" : [],
"deviceIndex" : 0,
"context" : context,
"accessToken" : accessToken
};
var q = exports.getJSON(options, callback, params);
};
//from RGB, HSV, and HSL color conversion algorithms in JavaScript · GitHub
/**
- Converts an HSL color value to RGB. Conversion formula
- adapted from HSL and HSV - Wikipedia.
- Assumes h, s, and l are contained in the set [0, 1] and
- returns r, g, and b in the set [0, 255].
-
-
@param Number h The hue
-
@param Number s The saturation
-
@param Number l The lightness
-
@return Array The RGB representation
*/
function hslToRgb(h, s, l) {
console.log(“called hslToRgb(” + h + “,” + s + “,” + l + “)”);
var r, g, b;
if (s == 0) {
r = g = b = l; // achromatic
} else {
function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [ r * 255, g * 255, b * 255 ];
}
// expected hue range: [0, 360)
// expected saturation range: [0, 1]
// expected lightness range: [0, 1]
var hslToRgb2 = function(hue, saturation, lightness){
console.log(“called hslToRgb(” + hue + “,” + saturation + “,” + lightness + “)”);
// based on algorithm from http://en.wikipedia.org/wiki/HSL_and_HSV#Converting_to_RGB
if( hue == undefined ){
return [0, 0, 0];
}
var chroma = (1 - Math.abs((2 * lightness) - 1)) * saturation;
var huePrime = hue / 60;
var secondComponent = chroma * (1 - Math.abs((huePrime % 2) - 1));
huePrime = Math.floor(huePrime);
var red;
var green;
var blue;
if( huePrime === 0 ){
red = chroma;
green = secondComponent;
blue = 0;
}else if( huePrime === 1 ){
red = secondComponent;
green = chroma;
blue = 0;
}else if( huePrime === 2 ){
red = 0;
green = chroma;
blue = secondComponent;
}else if( huePrime === 3 ){
red = 0;
green = secondComponent;
blue = chroma;
}else if( huePrime === 4 ){
red = secondComponent;
green = 0;
blue = chroma;
}else if( huePrime === 5 ){
red = chroma;
green = 0;
blue = secondComponent;
}
var lightnessAdjustment = lightness - (chroma / 2);
red += lightnessAdjustment;
green += lightnessAdjustment;
blue += lightnessAdjustment;
return [Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255)];
};
/**
- Converts an HSV color value to RGB. Conversion formula
- adapted from http://en.wikipedia.org/wiki/HSV_color_space.
- Assumes h, s, and v are contained in the set [0, 1] and
- returns r, g, and b in the set [0, 255].
-
-
@param Number h The hue
-
@param Number s The saturation
-
@param Number v The value
-
@return Array The RGB representation
*/
function hsvToRgb(h, s, v) {
console.log(“called hsvToRgb(” + h + “,” + s + “,” + v + “)”);
var r, g, b;
var i = Math.floor(h * 6);
var f = h * 6 - i;
var p = v * (1 - s);
var q = v * (1 - f * s);
var t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
return [ r * 255, g * 255, b * 255 ];
}
function cToF(celsius)
{
return parseInt(parseFloat(celsius) * 9.0 / 5.0 + 32.0);
}
function fToC(fahrenheit)
{
return parseInt((parseFloat(fahrenheit) - 32.0) * 5.0 / 9.0);
}