Waldemar Sakalus
Published © GPL3+

Smart Kettle with Alexa [Updated To v3 API]

Hacking a kettle to bring Alexa (and as a by-product IFTTT) connectivity.

IntermediateShowcase (no instructions)4 hours1,714
Smart Kettle with Alexa [Updated To v3 API]

Things used in this project

Hardware components

Photon
Particle Photon
×1
optocoupler 4n35
×2
led
×1
resistors 2x220ohm, 1x560ohm, 1x10kohm
×1
Programmable kettle Hamilton Beach 40996
×1

Story

Read more

Schematics

Smart Kettle schematics

Code

smartkettle.ino

Arduino
Code for the Particle
// Waldemar Sakalus- SAKA
// Ver 09162017
// Control a smart kettle via Alexa skills


int builtInLED = 7;
int extLED= 4;
int optoPin = 2;
int kettleState = 3;
byte sendSignalControl = LOW;
byte kettleReadPrevious = LOW;
byte watchKettle = HIGH;
byte sustainKettle = LOW;
unsigned long resetTimer = millis();
unsigned long sustainTimerInterval = 1200000; //interval to switch off the kettle 
unsigned long sustainTimer = millis();
unsigned long heartBeatInterval = 15000; //interval for blinking the led
unsigned long heartBeatTimer = millis();


void setup()
{
    //Serial.begin(9600);
    //Serial.println("Test");
    pinMode(builtInLED, OUTPUT);
    pinMode(extLED, OUTPUT);
    pinMode(optoPin, OUTPUT);
    pinMode(kettleState, INPUT);
    Particle.function("KettleOnOff", changeKettleStatus);

}

void loop() {
    byte kettleRead = digitalRead(kettleState);
    if (kettleRead) {
        digitalWrite(extLED, LOW);
        kettleRead = LOW;
    }
    else {
        digitalWrite(extLED, HIGH);
        kettleRead = HIGH;
    }
    
    // Function to turn off the kettle sustain after 20min. The kettle has a 60min built in, and one cannot change that parameter
    
    // check if kettle was switched on
    if (kettleRead && watchKettle) {
        watchKettle = LOW;
    }
    // if kettle was switched on, wait until gets off to start timer
    if (!kettleRead && !watchKettle & !sustainKettle) {
        sustainTimer = millis() + sustainTimerInterval;
        sustainKettle = HIGH;
        sendSignal(extLED);
    }
    // ater timer runs, switch off sustain
    if ((sustainTimer < millis()) && !watchKettle && sustainKettle) {
        watchKettle = HIGH;
        sustainKettle = LOW;
        togglePin();
    }
    
    //blink an LED - this is just here as a validation that it's working...
    if (heartBeatTimer < millis()) {
        sendHeartbeat(builtInLED);
        sendHeartbeat(extLED);
        heartBeatTimer = heartBeatTimer + heartBeatInterval;
    }
    if (sendSignalControl) {
        sendSignal(extLED);
        sendSignalControl = LOW;
    }
    // Reset after 24h of operation
    // ==================================
    if (millis() - resetTimer > 86400000) {
        System.reset();
    }
}

int changeKettleStatus(String command)
{
    int kettleRead = HIGH;
    if (digitalRead(kettleState)) kettleRead = LOW;
    if (command != NULL) {
        //Return codes:
        //1= kettle set on
        //2= kettle set off
        //3= no action, kettle was already on
        //4= is already onno action, kettle was already off
        if (command == "on" && kettleRead == LOW ) {
            togglePin();
            sendSignalControl = HIGH;
            return 1;
        }
        else if (command == "off" && kettleRead == HIGH) {
            togglePin();
            sendSignalControl = HIGH;
            return 2;
        }
        else if (command == "on" && kettleRead == HIGH ) {
            return 3;
        }
        else if (command == "off" && kettleRead == LOW ) {
            return 4;
        }
    }
    else return -1;
}

void sendHeartbeat(int whichLED) {
    //send a short heartbeat, so visible through the built in LED that the device is not hanging
    byte ledStatus = digitalRead(whichLED);
    byte tempLED = LOW;
    for (int i = 0; i < 5; i++) {
        digitalWrite(whichLED, tempLED); // Write LED high/low
        tempLED = (tempLED == HIGH) ? LOW : HIGH;
        delay(30);
    }
    digitalWrite(whichLED, ledStatus); //restore the LED status
}

void sendSignal(int whichLED) {
    //send a short heartbeat, so visible through the built in LED that the device is not hanging
    byte ledStatus = digitalRead(whichLED);
    byte tempLED = LOW;
    for (int i = 0; i < 5; i++) {
        digitalWrite(whichLED, tempLED); // Write LED high/low
        tempLED = (tempLED == HIGH) ? LOW : HIGH;
        delay(300);
    }
    digitalWrite(whichLED, ledStatus); //restore the LED status
}

void togglePin () {
    unsigned long toggleSwitchDelay = 1000;
    digitalWrite(optoPin, HIGH);
    delay(toggleSwitchDelay);
    digitalWrite(optoPin, LOW);
}

smartkettle.js

JavaScript
This is the code for Lambda function
var https = require('https');

var serialPortControllerId = "SAKA_Kettle_10102017";

var particleServer = "api.particle.io";
var particlePath = "/v1/devices/";
var particleId = "your_device_id"; //you can find this in the particle IDE console under devices

var powerOn = "on";
var powerOff = "off";

exports.handler = function(event, context) {

    log('Input', event);

    switch (event.directive.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.Discovery':
            handleDiscovery(event, context);
            break;

        case 'Alexa.PowerController':
            handleControl(event, context);
            break;

        default:
            log('Err', 'No supported namespace: ' + event.directive.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(event, context) {

    var payload = {
        "endpoints":[
            {
            "endpointId": serialPortControllerId,
            "friendlyName": "Kettle",
            "description": "Smart Kettle by SAKA (c) 2017",
            "manufacturerName": "SAKA",
            "displayCategories": ["OTHER"],
            "cookie": {
                "deviceId" : particleId
                },
            "capabilities": [
            {
                "type": "AlexaInterface",
                "interface": "Alexa.PowerController",
                "version": "1.0",
                "properties": {
                    "supported" : [
                        {
                            "name": "powerState"
                        }
                    ],
                    "proactivelyReported": true,
                    "retrievable": true
                }
            }
        ]}
    ]};


    /**
     * Craft the final response back to Alexa Connected Home Skill. This will include all the 
     * discoverd appliances.
     */
    var header = event.directive.header;
    header.name = "Discover.Response";
    context.succeed({ event: {
    header: header, payload: payload
    } });

    //log('Discovery', result);

}

/**
 * Control events are processed here.
 * This is called when Alexa requests an action (IE turn off appliance).
 */
function handleControl(event, context) {
    var namespace = event.directive.header.namespace;
    var messageId = event.directive.header.messageId;
    var correlationToken = event.directive.header.correlationToken;
    var accessToken = event.directive.endpoint.scope.token;
    var endpointId = event.directive.endpoint.endpointId;
    var deviceId = event.directive.endpoint.cookie.deviceId;
    var name = "";
    var value = "";
    var UTCDate = new Date();
    var jsonDate = JSON.stringify(UTCDate);
    var strDate = JSON.parse(jsonDate);  
    var funcName = "KettleOnOff";
    if (namespace === "Alexa.PowerController") {
        
        var param = "";
        var index = "0";
        var executeParticle = true;

        if (namespace === "Alexa.PowerController") {
            if(event.directive.header.name == "TurnOn"){
                kettleCommand = powerOn;
                name = "powerState";
                value = "ON";
            }
            else if(event.directive.header.name == "TurnOff"){
                kettleCommand = powerOff;            
                name = "powerState";
                value = "OFF";
            }
        }

        if(deviceId == particleId){
            index = "0";
        }

        param = kettleCommand;
        
        var options = {
            hostname: particleServer,
            port: 443,
            path: particlePath + deviceId + "/" + funcName,
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        };
        
        log(options);
        
        var data = "access_token=" + accessToken + "&" + "args=" + param;
        
        log(data);

        var serverError = function (e) {
            log('Error', e.message);
            context.fail(generateControlError(event, 'ENDPOINT_UNREACHABLE', 'Unable to connect to server'));
        };

        if (executeParticle) {
            var callback = function(response) {
                var str = '';
    
                response.on('data', function(chunk) {
                    str += chunk.toString('utf-8');
                });
    
                response.on('end', function() {
                    log('Return Value');
                    log(str);
                    
                var result = {
                    "context": {
                        "properties": [ {
                            "namespace": namespace,
                            "name": name,
                            "value": value,
                            "timeOfSample": strDate,
                            "uncertaintyInMilliseconds": 500
                            } ]
                        },
                          "event": {
                            "header": {
                              "namespace": "Alexa",
                              "name": "Response",
                              "payloadVersion": "3",
                              "messageId": messageId,
                              "correlationToken": correlationToken
                            },
                            "endpoint": {
                              "scope": {
                                "type": "BearerToken",
                                "token": accessToken
                              },
                              "endpointId": endpointId
                            },
                            "payload": {}
                          }
                        };
                    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(event, code, description) {
    var messageId = event.directive.header.messageId;
    var correlationToken = event.directive.header.correlationToken;
    var accessToken = event.directive.endpoint.scope.token;
    var endpointId = event.directive.endpoint.endpointId;
    var response = {
        "header": {
            "namespace": "Alexa",
            "name": "ErrorResponse",
            "messageId": messageId,
            "correlationToken": correlationToken,
            "payloadVersion": "3"
            },
            "endpoint":{
                "scope":{
                    "type":"BearerToken",
                    "token":accessToken
                },
                "endpointId":endpointId
            },
            "payload": {
                "type": code,
                "message": description
            }
        };

    var result = { event : {response}};

    return result;
}

//Function to format the volume with leading zeros
function pad(num, size) {
    var s = "000" + num;
    return s.substr(s.length-size);
}

Credits

Waldemar Sakalus

Waldemar Sakalus

4 projects • 18 followers

Comments