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

Run Google Assistant on Your Amazon Echo
Intermediate
  • 2,210
  • 21

Full instructions

This is an implementation of the Google Assistant API wrapped as an Alexa skill.

UK Treasure Hunt
Intermediate
  • 76
  • 4

Full instructions

Find Precious Treasures from across the United Kingdom..!

Arduino 101 Packet Radio IMU
Intermediate
  • 499
  • 2

Packet Radio on your Arduino 101 - The Arduino 101 Orientation Visualizer redone with Packet Radio!

Know Your MSP
Intermediate
  • 20
  • 1

Full instructions

Unfamiliar with Scottish politics? Know Your MSP aims to help you understand your MSP and constituency.

ArduRadio AlarmClock
Intermediate
  • 2,380
  • 2

Full instructions

Build an FM radio with alarm clock.

Parliament Election Support from your Alexa
Intermediate
  • 76
  • 3

Full instructions

Democracy calls! Use this Alexa skill for who is running for the UK Parliament on the snap election for the 8th of June, as well as who won.

ProjectsCommunitiesTopicsContestsLiveAppsBetaFree StoreBlogAdd projectSign up / Login