Things used in this project

Hardware components:
Photon new
Particle Photon
×1
1586 00
Adafruit NeoPixel Ring: WS2812 5050 RGB LED
Any FastLED support RGB LED will actually work including rings, strips and individual lights.
×1
Software apps and online services:
Amazon Alexa Home Automation skill
FastLED

Custom parts and enclosures

Steps to Create a Smart Home Skill
This is a PDF copy of the link in the instructions
HOWTO Add Oauth to your Alexa Smart Home Skills in 10 Minutes
This is a PDF Copy of the link in the instructions

Code

Alexa Lambda Code (Python 3.6)Python
This code acts as a gateway between Alexa and the Particle Cloud. To use the code, make sure to insert your particle Token and Particle Device ID. This supports 4 Alexa devices - and you could easily add more, however, you don't have to control 4 phyical devices. For example, you could have one device called "Bedroom" and one called "Bedroom Party" and the bedroom party could turn on the same lights, but with a different scene in FastLED (like rainbow lights or sparkles).
#-------------------------------------------------------------------------#
#
#  Charlotte IOT - Alexa to Particle IO Gateway for Fan and Lights    
#    written by: Jeremy Proffitt - Licensed for non commercial use only
#
#-------------------------------------------------------------------------#
#
#  NOTE: When you make changes to this script, you likely have to force
#        a rediscovery of Home Automation devices in the Alexa App.
#
#  To configure, change the following variables:

#Alexs device names
lightName = "Spare Bedroom";
fanName = "Fan";
spareOnOff1Name = "Back Door";
spareOnOff2Name = "Back Room";

#Particle Information:
particleToken = "**INSERT YOUR TOKEN HERE**";
particleDeviceId = "**INSERT YOUR DEVICE HERE**";
particleLightFunction = "setLight";
particleFanFunction = "setFan";
particleSpareOnOff1Function = "setOutput1";
particleSpareOnOff2Function = "setOutput2";

#configuration for Alexa
#  set the device type below using the below Enum.
from enum import Enum
class DeviceType(Enum):
    OnOff = 1
    Percent = 2
    Color = 3
    Lock = 4
    Disabled = 5

lightType = DeviceType.Color; 
fanType = DeviceType.Percent;  
spareOnOff1Type = DeviceType.Lock;
spareOnOff2Type = DeviceType.Color;


####################################################################################################
#
#                 DO NOT CHANGE ANYTHING BELOW THIS LINE.
#
####################################################################################################

import urllib
import json
import urllib.request
import urllib.parse

def lambda_handler(event, context):
    #sumoLog(event, context);
    access_token = event['payload']['accessToken']

    if event['header']['namespace'] == 'Alexa.ConnectedHome.Discovery':
        return handleDiscovery(context, event)

    elif event['header']['namespace'] == 'Alexa.ConnectedHome.Control':
        return handleControl(context, event)

def handleDiscovery(context, event):
    payload = ''

    header = {
        "namespace": "Alexa.ConnectedHome.Discovery",
        "name": "DiscoverAppliancesResponse",
        "payloadVersion": "2"
        }
        
    if event['header']['name'] == 'DiscoverAppliancesRequest':
        payload = {
            "discoveredAppliances":[
                {
                    "applianceId":particleLightFunction,
                    "manufacturerName":"CharlotteIOT",
                    "modelName":"IOT",
                    "version":"1",
                    "friendlyName":lightName,
                    "friendlyDescription":lightName,
                    "isReachable":True,
                    "actions":[
                        "turnOn",
                        "turnOff"
                    ],
                    "additionalApplianceDetails":{
                        "extraDetail1":"There are no extra details."
                    }
                },
                {
                    "applianceId":particleFanFunction,
                    "manufacturerName":"CharlotteIOT",
                    "modelName":"IOT",
                    "version":"1",
                    "friendlyName":fanName,
                    "friendlyDescription":fanName,
                    "isReachable":True,
                    "actions":[
                        "turnOn",
                        "turnOff"
                    ],
                    "additionalApplianceDetails":{
                        "extraDetail1":"There are no extra details."
                    }
                },
                {
                    "applianceId":particleSpareOnOff1Function,
                    "manufacturerName":"CharlotteIOT",
                    "modelName":"IOT",
                    "version":"1",
                    "friendlyName":spareOnOff1Name,
                    "friendlyDescription":spareOnOff1Name,
                    "isReachable":True,
                    "actions":[
                        "turnOn",
                        "turnOff"
                    ],
                    "additionalApplianceDetails":{
                        "extraDetail1":"There are no extra details."
                    }
                },
                {
                    "applianceId":particleSpareOnOff2Function,
                    "manufacturerName":"CharlotteIOT",
                    "modelName":"IOT",
                    "version":"1",
                    "friendlyName":spareOnOff2Name,
                    "friendlyDescription":spareOnOff2Name,
                    "isReachable":True,
                    "actions":[
                        "turnOn",
                        "turnOff"
                    ],
                    "additionalApplianceDetails":{
                        "extraDetail1":"There are no extra details."
                    }
                }
            ]
        }
        
        if spareOnOff2Type == DeviceType.Disabled:
            del payload['discoveredAppliances'][3];
        elif spareOnOff2Type == DeviceType.Lock:
            payload['discoveredAppliances'][3]['actions'].remove("turnOn");
            payload['discoveredAppliances'][3]['actions'].remove("turnOff");
            payload['discoveredAppliances'][3]['actions'].append("setLockState");
        elif spareOnOff2Type == DeviceType.Percent or spareOnOff2Type == DeviceType.Color:
            payload['discoveredAppliances'][3]['actions'].append("decrementPercentage");
            payload['discoveredAppliances'][3]['actions'].append("incrementPercentage");
            payload['discoveredAppliances'][3]['actions'].append("setPercentage");
        if spareOnOff2Type == DeviceType.Color:
            payload['discoveredAppliances'][3]['actions'].append("setColor");
        
            
        if spareOnOff1Type == DeviceType.Disabled:
            del payload['discoveredAppliances'][2];
        elif spareOnOff1Type == DeviceType.Lock:
            payload['discoveredAppliances'][2]['actions'].remove("turnOn");
            payload['discoveredAppliances'][2]['actions'].remove("turnOff");
            payload['discoveredAppliances'][2]['actions'].append("setLockState");
        elif spareOnOff1Type == DeviceType.Percent or spareOnOff1Type == DeviceType.Color:
            payload['discoveredAppliances'][2]['actions'].append("decrementPercentage");
            payload['discoveredAppliances'][2]['actions'].append("incrementPercentage");
            payload['discoveredAppliances'][2]['actions'].append("setPercentage");
        if spareOnOff1Type == DeviceType.Color:
            payload['discoveredAppliances'][2]['actions'].append("setColor");
        if spareOnOff1Type == DeviceType.Lock:
            payload['discoveredAppliances'][2]['actions'].append("setLockState");
        
        if fanType == DeviceType.Disabled:
            del payload['discoveredAppliances'][1];
        elif fanType == DeviceType.Lock:
            payload['discoveredAppliances'][1]['actions'].remove("turnOn");
            payload['discoveredAppliances'][1]['actions'].remove("turnOff");
            payload['discoveredAppliances'][1]['actions'].append("setLockState");
        elif fanType == DeviceType.Percent or spareOnOff2Type == DeviceType.Color:
            payload['discoveredAppliances'][1]['actions'].append("decrementPercentage");
            payload['discoveredAppliances'][1]['actions'].append("incrementPercentage");
            payload['discoveredAppliances'][1]['actions'].append("setPercentage");
        if fanType == DeviceType.Color:
            payload['discoveredAppliances'][1]['actions'].append("setColor");       
        if spareOnOff1Type == DeviceType.Lock:
            payload['discoveredAppliances'][1]['actions'].append("setLockState");
            
        if lightType == DeviceType.Disabled:
            del payload['discoveredAppliances'][0];
        elif lightType == DeviceType.Lock:
            payload['discoveredAppliances'][0]['actions'].remove("turnOn");
            payload['discoveredAppliances'][0]['actions'].remove("turnOff");
            payload['discoveredAppliances'][0]['actions'].append("setLockState");
        elif lightType == DeviceType.Percent or lightType == DeviceType.Color:
            payload['discoveredAppliances'][0]['actions'].append("decrementPercentage");
            payload['discoveredAppliances'][0]['actions'].append("incrementPercentage");
            payload['discoveredAppliances'][0]['actions'].append("setPercentage");
        if lightType == DeviceType.Color:
            payload['discoveredAppliances'][0]['actions'].append("setColor");       
            
            
    print(json.dumps(payload));
    return { 'header': header, 'payload': payload }

def handleControl(context, event):
    deviceId = event['payload']['appliance']['applianceId'];
    messageId = event['header']['messageId'];
    responseName = '';
    payload = { };
    
    print("handleControl Called.");
    print(deviceId);
    print(messageId);
    
    #Turn On
    if event['header']['name'] == 'TurnOnRequest':
        responseName = 'TurnOnConfirmation';
        sendToParticle(deviceId, "on");
        
    #TurnOff
    elif event['header']['name'] == 'TurnOffRequest':
        responseName = 'TurnOffConfirmation';
        sendToParticle(deviceId, "off");
    
    #setPercent
    elif event['header']['name'] == 'SetPercentageRequest':
        responseName = 'SetPercentageConfirmation';
        percent = event['payload']['percentageState']['value']
        sendToParticle(deviceId, percent);
    
    #decreasePercent
    elif event['header']['name'] == 'DecrementPercentageRequest':
        responseName = 'DecrementPercentageConfirmation';
        percent = event['payload']['deltaPercentage']['value']
        sendToParticle(deviceId, '-' + str(percent));
        
    #increasePercent
    elif event['header']['name'] == 'IncrementPercentageRequest':
        responseName = 'IncrementPercentageConfirmation';
        percent = event['payload']['deltaPercentage']['value']
        sendToParticle(deviceId, '+' + str(percent));
       
    #Color
    elif event['header']['name'] == 'SetColorRequest':
        print(event);
        responseName = 'SetColorConfirmation';
        hue = event['payload']['color']['hue'];
        saturation = event['payload']['color']['saturation'];
        brightness = event['payload']['color']['brightness'];
        sendToParticle(deviceId, "color:" + str(hue / 360 * 255) + ":" + str(saturation * 255) + ":" + str(brightness * 255));
        payload = {
            "achievedState": 
                {
                "color": 
                    {
                    "hue": 0.0,
                    "saturation": 1.0000,
                    "brightness": 1.0000
                    }
                }
            }
        
    #Lock/Unlock
    elif event['header']['name'] == 'SetLockStateRequest':
        responseName = 'TurnOnConfirmation';
        lockState = event['payload']['lockState']
        sendToParticle(deviceId, lockState);
        payload = {
            "lockState": lockState
        }
        
    #HealthCheck
    elif event['header']['name'] == 'HealthCheckRequest':
        responseName = 'HealthCheckResponse';
        sendToParticle(deviceId, "healthcheck");
        payload = {
            "description": "The system is currently healthy",
            "isHealthy": "true"
        }
    
    
    
    
    header = {
            "namespace":"Alexa.ConnectedHome.Control",
            "name":responseName,
            "payloadVersion":"2",
            "messageId": messageId
            }
    return { 'header': header, 'payload': payload }

def sendToParticle(function, value):
    print('sendToParticle({0}, {1})'.format(function, value));
    url = "https://api.particle.io/v1/devices/" + particleDeviceId + "/" + function;
    print('url: {0}'.format(url));
    body = urllib.parse.urlencode({'arg' : value , 'access_token' : particleToken});
    data = body.encode('ascii');
    urllib.request.urlopen(url, data=data);
    return;
    
Particle IO Code (Firmware 0.6.2-rc.2)C/C++
**You must use firmware 0.6.2-rc.2, 0.6.1 will not work** I've left a few troubleshooting items in the code, including the manual color assignments in updateLights, useful if your RGB led's are set up in a different order. If they are, then just change the GRB in the FastLED.addLeds line to what ever the order should be. By default we are set up on Pin 6 for the LED output, but you can change this in the FastLED.addLeds line.
// This #include statement was automatically added by the Particle IDE.
#include <FastLED.h>

FASTLED_USING_NAMESPACE;
#define PARTICLE_NO_ARDUINO_COMPATIBILITY 1
#include "Particle.h"

#define NUM_LEDS 60

CRGB leds[NUM_LEDS];


int _lightLevel = 100;

int _hue = 260;
int _saturation = 255;
int _brightness = 255;
int _adjustedBrightness = 100;


void setup() { 
    FastLED.addLeds<WS2812, 6, GRB>(leds, NUM_LEDS);
    
    Particle.function("setLight", setLight);
    Particle.variable("lightLevel", _lightLevel);
    Particle.variable("hue",_hue);
    Particle.variable("saturation",_saturation);
    Particle.variable("brightness",_brightness);
    Particle.variable("aBrightness",_adjustedBrightness);
    updateLights();
}

void loop() { 
    
   
    
}

void updateLights() {
    
    if (_lightLevel < 10 and _lightLevel > 0) {
            _lightLevel = 10;
    }
    
    
    _adjustedBrightness = _brightness * _lightLevel / 100;
    
    // HSV (Spectrum) to RGB color conversion
    CHSV hsv( _hue, _saturation, _adjustedBrightness); // pure blue in HSV Spectrum space
    CRGB rgb;
    hsv2rgb_spectrum( hsv, rgb);
    
    
    for (int i = 0; i < NUM_LEDS; i++) {
        leds[i] = CRGB(rgb);//CHSV( _hue, _saturation, _adjustedBrightness);
    }
    /*
    leds[0] = CRGB::Black;
    leds[1] = CRGB::Red;
    leds[2] = CRGB::Green;
    leds[3] = CRGB::Blue;
    leds[4] = CRGB::Blue;
    leds[5] = CRGB::Black;
    */
    
    FastLED.show();
}


int setLight(String level) {
    _lightLevel = translateLevel(level, _lightLevel);
    updateLights();
}


int stoi(String number) {
    char inputStr[64];
    number.toCharArray(inputStr,64);
    return atoi(inputStr);
}

/*---------------------------------------------------------------
 * translateLevel takes a string input from the Alexa controller
 *  and converts it into a level between 0 and 100
/---------------------------------------------------------------*/
int translateLevel(String level, int currentLevel)
{
    level = level.toUpperCase();
    if (level == "ON") {
         currentLevel = 100;
    } 
    else if (level == "OFF") {
        currentLevel = 0;
    } 
    else if (level.substring(0,1) == "+") {
        level = level.substring(1,level.length());
        currentLevel += stoi(level);
    } 
    else if (level.substring(0,1) == "-") {
        level = level.substring(1,level.length());
        currentLevel -= stoi(level);
    } 
    else if (level.substring(0,6) == "COLOR:") {
        level = level.substring(6,level.length());
        _hue = stoi(getValue(level,':',0));
        _saturation = stoi(getValue(level,':',1));
        _brightness = stoi(getValue(level,':',2));
        if (_lightLevel == 0)  {
            _lightLevel == 100;
        } 
    } else {
        currentLevel = stoi(level);
    }
   
    //If the current level is out of range, return top or bottom of the range.
    if (currentLevel > 100) return 100;
    if (currentLevel < 0) return 0;
    return currentLevel;
}

String getValue(String data, char separator, int index)
{
    int found = 0;
    int strIndex[] = { 0, -1 };
    int maxIndex = data.length() - 1;

    for (int i = 0; i <= maxIndex && found <= index; i++) {
        if (data.charAt(i) == separator || i == maxIndex) {
            found++;
            strIndex[0] = strIndex[1] + 1;
            strIndex[1] = (i == maxIndex) ? i+1 : i;
        }
    }
    return found > index ? data.substring(strIndex[0], strIndex[1]) : "";
}

Credits

2017 03 10 12 41 56 lrhs5zx7ve
Jeremy Proffitt

An SRE by trade, I'm the jack of all trades, master of none with an understanding of failure, risk and reward. I love to create and fix.

Contact

Replications

Did you replicate this project? Share it!

I made one

Love this project? Think it could be improved? Tell us what you think!

Give feedback

Comments

Similar projects you might like

Lightweight Sliding Door Automator
Intermediate
  • 1,535
  • 7

Work in progress

Automate the opening and closing of a lightweight sliding door (e.g. a screen door), including remote controls on your phone.

Christmas Gift Box
Intermediate
  • 3,633
  • 595

Full instructions

Christmas Gift Box plays music and sends an email when it is opened.

Carbon Fiber Vacuum Chamber
Intermediate
  • 2,905
  • 94

Full instructions

Our project is a carbon fiber vacuum chamber that is monitored by multiple particle photons and various sensors.

ConnectTheDots with Particle Azure IoT Hub Integration
Intermediate
  • 841
  • 8

Protip

This project will allow you to connect your Particle device into an Azure IoT Hub for viewing data in real-time through an ASP.NET web app!

What should I wear outside?
Intermediate
  • 5,044
  • 67

Full instructions

A whimsical weather clock powered by Particle and forecast.io

Garage Door Opener with Blynk
Intermediate
  • 367
  • 2

Open, close and monitor a garage door opener with a smart phone using a Particle Photon and the Blynk application.

Add projectSign up / Login