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

Raspberry Pi Motion Tracking Gun Turret
Intermediate
  • 7
  • 2

Full instructions

A motion tracking airsoft or nerf gun turret with autonomous and manual operation modes, controlled by a Raspberry Pi.

Reducing Arduino Power Consumption
Intermediate
  • 113
  • 5

Full instructions

One the most important feature of portable electronics should be long battery life. We can reduce the current drawn by several ways.

Particle Photon Based Security System with Alerting
Intermediate
  • 1,318
  • 20

Full instructions

Be alerted to any door or window opening and closing in your house on your smart phone.

My Guardian for the Workshop
Intermediate
  • 188
  • 4

Full instructions

Device will send emails if the door or window has been opened, and will monitor the temperature and humidity of the place.

Audio DAC Hat for Pi with Headphone Jack and 3W Speaker Out
Intermediate
  • 22
  • 1

Full instructions

Make your own DAC hat for Raspberry Pi 3, Raspberry Pi Zero, Raspberry Pi Zero W that has a Headphone jack and 3W Stereo speaker out.

Happy Plant Notifier
Intermediate
  • 278
  • 5

Full instructions

Get a text or call if your plant is getting too dry, along with a nice LCD display giving information regarding your plant's health.

Sign up / LoginProjectsPlatformsTopicsContestsLiveAppsBetaFree StoreBlog