Jeremy Proffitt
Published © CC BY-NC

Full Color Alexa Controlled Lights - FastLED & Photon

Full color light controlled by Alexa. This is a step by step using a Particle Photon and configurable Python Script in Lambda.

IntermediateFull instructions provided4 hours3,221
Full Color Alexa Controlled Lights - FastLED & Photon

Things used in this project

Hardware components

Photon
Particle Photon
×1
NeoPixel Ring: WS2812 5050 RGB LED
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 Smart Home Skill API
FastLED

Story

Read more

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

Jeremy Proffitt

Jeremy Proffitt

1 project • 7 followers
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.

Comments