Smartified Things
Published © CC BY-NC-ND

Smartified Stove Control with LEGO EV3 and Alexa

This is a stove control that can be operated through voice commands using Amazon Alexa. It can shut off a stove based on a timer

BeginnerFull instructions provided24 hours31

Things used in this project

Story

Read more

Code

Alexa Gadget on EV3 - Stove Control

Python
This is an Alexa Gadget on EV3 that processes directives sent from an Alexa Skill to shut off a stove based on a timer
#!/usr/bin/env python3

import shutil
import time
import logging
import json
import threading

from agt import AlexaGadget

from ev3dev2.led import Leds
from ev3dev2.sound import Sound
from ev3dev2.motor import OUTPUT_A, LargeMotor

# Set the logging level to INFO to see messages from AlexaGadget
logging.basicConfig(level=logging.INFO)

class MindstormsGadget(AlexaGadget):
    """
    A Mindstorms gadget that shuts off a stove based on a timer configured by voice commands.
    """
  
    def __init__(self):
        """
        Performs Alexa Gadget initialization routines and ev3dev resource allocation.
        """
        super().__init__()

        # Gadget state
        self.current_position = 12
        self.remaining_sec = 0
        self.flashing = 0
        self.timer_end = time.time()
        self.columns = shutil.get_terminal_size().columns
        self.lines = shutil.get_terminal_size().lines


        # Ev3dev initialization
        self.leds = Leds()
        self.sound = Sound()
        self.drive = LargeMotor(OUTPUT_A)

    def on_connected(self, device_addr):
        """
        Gadget connected to the paired Echo device.
        :param device_addr: the address of the device we connected to
        """
        self.leds.set_color("LEFT", "GREEN")
        self.leds.set_color("RIGHT", "GREEN")
        print("{} connected to Echo device".format(self.friendly_name))

    def on_disconnected(self, device_addr):
        """
        Gadget disconnected from the paired Echo device.
        :param device_addr: the address of the device we disconnected from
        """
        self.leds.set_color("LEFT", "BLACK")
        self.leds.set_color("RIGHT", "BLACK")
        print("{} disconnected from Echo device".format(self.friendly_name))

    def on_custom_mindstorms_gadget_control(self, directive):
        """
        Handles the Custom.Mindstorms.Gadget control directive.
        :param directive: the custom directive with the matching namespace and name
        """
        try:
            payload = json.loads(directive.payload.decode("utf-8"))
            print("Control payload: {}".format(payload))
            control_type = payload["type"]

            if control_type == "StoveOffTimer":
                # Expected params: [delay]
                self._stoveOffTimer(payload["delay"])
            elif control_type == "Calibrate":
                # Expected params: [position]
                self._calibrate(int(payload["position"]))
            elif control_type == "Rotate":
                # Expected params: [degrees]
                count = int(payload["degrees"])
                self._rotate(count)
            elif control_type == "ReverseRotate":
                # Expected params: [degrees]
                count = int(payload["degrees"])*-1
                self._rotate(count)
            elif control_type == "Move":
                # Expected params: [position]
                position = int(payload["position"])
                if (position > 6 and position < 9):
                    sound.speak("{} is not a valid position".format(position))
                    return
                if (position >=9 and position <= 12):
                    position = position - 12
                self._move(position)

        except KeyError:
            print("Missing expected parameters: {}".format(directive))


    def _rotate(self, count):
        self.drive.on_for_degrees(speed=10, degrees=count)

    def _move(self, position: int):
        if ((position > 6 and position < 9) or position > 12):
            self.sound.speak("{} is not a valid position".format(position))
            return


        to_move = (position - int(self.current_position)) * 30
        self.current_position = position
        self.drive.on_for_degrees(speed=10, degrees=to_move)

    def _calibrate(self, position: int):
        """
        Calibtrate the currnt position
        """
        if ((position > 6 and position < 9) or position > 12):
            self.sound.speak("{} is not a valid position".format(position))
            return
        if (position >=9 and position <= 12):
            position = position - 12

        self.current_position = position
        print("The current knob position was calibrated as {}".format(self.current_position))

    def _stoveOffTimer(self, delay: int):
        """
        Handles stove commands from the directive.
        """
        self.timer_end = time.time() + int(delay)
        self.remaining_sec = int(delay)
        mins = int(self.remaining_sec / 60)
        secs = int(self.remaining_sec % 60)

        mins_str = ""
        secs_str = ""

        if (mins != 0):
           if (mins == 1):
              mins_str = "one minute"
           else:
              mins_str = "{} minutes".format(mins)
           if (secs != 0):
              mins_str = mins_str + " and"

        if (secs != 0):
           if (secs == 1):
             secs_str = " one second"
           else:
             secs_str = " {} seconds".format(secs)

        print("The stove shutoff timer will be triggered in {}{}".format(mins_str, secs_str))
        self.sound.speak("The stove shutoff timer will be triggered in {}{}".format(mins_str, secs_str))
        
        c = 0

        while (c < self.lines):
            print("")
            c = c + 1

        self.flashing = 1
        timer_flash = threading.Timer(0.001, ledsFlash)
        timer_flash.start()
        timer_off = threading.Timer(0.001, stoveShutOff)
        timer_off.start()


def ledsFlash():
    global gadget

    if (gadget.flashing == 0):
        return

    if (gadget.flashing == 1):
        if (gadget.remaining_sec > 15):
            gadget.leds.set_color("LEFT", "YELLOW")
            gadget.leds.set_color("RIGHT", "YELLOW")
        else:
            gadget.leds.set_color("LEFT", "RED")
            gadget.leds.set_color("RIGHT", "RED")

        gadget.flashing = 2

    elif (gadget.flashing == 2):
        gadget.leds.set_color("LEFT", "BLACK")
        gadget.leds.set_color("RIGHT", "BLACK")
        gadget.flashing = 1
    timer = threading.Timer(0.46, ledsFlash)
    timer.start()

def stoveShutOff():
    global gadget

    gadget.remaining_sec = int(gadget.timer_end - time.time())
    if (gadget.remaining_sec > 0):
        timer_off = threading.Timer(1, stoveShutOff)
        timer_off.start()
        if (int(gadget.remaining_sec/5)*5 == gadget.remaining_sec):
           print("{} seconds left".format(gadget.remaining_sec).center(gadget.columns))
        return
    print("Turning off the stove...".center(gadget.columns))

    gadget.flashing = 0
    to_move = (6-int(gadget.current_position))*30
    gadget.sound.play_song((('E5', 'e'), ('E5', 'e')))
    gadget.sound.speak("turning off the stove")
    gadget.drive.on_for_degrees(speed=10, degrees=to_move)
    gadget.leds.set_color("LEFT", "BLACK")
    gadget.leds.set_color("RIGHT", "BLACK")
    gadget.sound.speak("Alexa", volume=100)
    gadget.sound.play_file("/home/robot/speech.wav", volume=100)

    print("Done.")

gadget = MindstormsGadget()

if __name__ == '__main__':

    # Startup sequence
    gadget.sound.play_song((('C4', 'e'), ('D4', 'e'), ('E5', 'q')))
    gadget.leds.set_color("LEFT", "GREEN")
    gadget.leds.set_color("RIGHT", "GREEN")

    # Gadget main entry point
    gadget.main()

    # Shutdown sequence
    gadget.sound.play_song((('E5', 'e'), ('C4', 'e')))
    gadget.leds.set_color("LEFT", "BLACK")
    gadget.leds.set_color("RIGHT", "BLACK")

Alexa Skill for EV3 - Stove Control

JavaScript
This is an Alexa Skill that processes voice commands and sends directives to an Alexa Gadget on EV3 to shut off a stove based on a timer
/*
 * This is an Alexa Skill named "Stove Control". Its main function is to turn off a stove by rotating the knob
 * to its off position based on a timer.
 * 
 * The main intent is "StoveOffTimerIntent".
 *
 * Other intents are used to calibrate the current position of the knob so that EV3 knows the offset to the off 
 * position.
*/

const Alexa = require('ask-sdk-core');
const Util = require('./util');
const Common = require('./common');

// The audio tag to include background music
const BG_MUSIC = '<audio src="soundbank://soundlibrary/ui/gameshow/amzn_ui_sfx_gameshow_waiting_loop_30s_01"></audio>';


// The namespace of the custom directive to be sent by this skill
const NAMESPACE = 'Custom.Mindstorms.Gadget';

// The name of the custom directive to be sent this skill
const NAME_CONTROL = 'control';

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    handle: async function(handlerInput) {

        let request = handlerInput.requestEnvelope;
        let { apiEndpoint, apiAccessToken } = request.context.System;
        let apiResponse = await Util.getConnectedEndpoints(apiEndpoint, apiAccessToken);
        if ((apiResponse.endpoints || []).length === 0) {
            return handlerInput.responseBuilder
            .speak(`I couldn't find an EV3 Brick connected to this Echo device. Please check to make sure your EV3 Brick is connected, and try again.`)
            .getResponse();
        }

        // Store the gadget endpointId to be used in this skill session
        let endpointId = apiResponse.endpoints[0].endpointId || [];
        Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);

        // Set skill duration to 5 minutes (ten 30-seconds interval)
        //Util.putSessionAttribute(handlerInput, 'duration', 10);

        // Set the token to track the event handler
        const token = handlerInput.requestEnvelope.request.requestId;
        Util.putSessionAttribute(handlerInput, 'token', token);


        return handlerInput.responseBuilder
            .speak("Welcome to smartified stove, you can start issuing stove control commands")
            .reprompt("Awaiting commands" + BG_MUSIC)
            .getResponse();
    }
};

const StoveOffTimerIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'StoveOffTimerIntent';
    },
    handle: function (handlerInput) {
        const request = handlerInput.requestEnvelope;
        var timer_mins = Alexa.getSlotValue(request, 'timer_mins');
        var timer_secs = Alexa.getSlotValue(request, 'timer_secs');
        
        if (timer_mins === undefined){
            timer_mins = 0;
        }
        else {
            timer_mins = parseInt(timer_mins);
        }
        
        if (timer_secs === undefined){
            timer_secs = 0;
        }
        else {
            timer_secs = parseInt(timer_secs);
        }
        
        const delay = parseInt(timer_mins) * 60 + parseInt(timer_secs);

        const attributesManager = handlerInput.attributesManager;
        const endpointId = attributesManager.getSessionAttributes().endpointId || [];
        Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);

        // Construct the directive with the payload containing the stove control parameters
        const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
            {
                type: "StoveOffTimer",
                delay: delay
            });
            
        var secs_str = "";
        
        if (timer_secs !== 0){
            if (timer_secs === 1){
                secs_str = "one second";
            }
            else {
                secs_str = timer_secs + "seconds";
            }
        }
        
        var mins_str = "";
        
        if (timer_mins !== 0){
            if (timer_mins === 1){
                mins_str = "one minute";
            }
            else {
                mins_str = timer_mins + " minutes";     
            }
            
            if (timer_secs > 0){
                mins_str = mins_str + " and ";
            }
        }


        return handlerInput.responseBuilder
                .speak("the stove will be turned off in " + mins_str + secs_str)
                .addDirective(directive)
                .getResponse();    
    }
};

const MoveIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'MoveToPositionIntent';
    },
    handle: function (handlerInput) {
        const request = handlerInput.requestEnvelope;
        const hour = Alexa.getSlotValue(request, 'hour');

        const attributesManager = handlerInput.attributesManager;
        const endpointId = attributesManager.getSessionAttributes().endpointId || [];
        Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);

        // Construct the directive with the payload containing the move parameters
        const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
            {
                type: 'Move',
                position: hour
            });

        
        return handlerInput.responseBuilder
            .speak("Rotating the knob to " + hour + " o'clock")
            .reprompt("awaiting command")
            .addDirective(directive)
            .getResponse();
    }
};


const ReverseRotateIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'ReverseRotateIntent';
    },
    handle: function (handlerInput) {
        const request = handlerInput.requestEnvelope;
        const count = Alexa.getSlotValue(request, 'count');

        const attributesManager = handlerInput.attributesManager;
        const endpointId = attributesManager.getSessionAttributes().endpointId || [];
        Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);

        // Construct the directive with the payload containing the move parameters
        const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
            {
                type: 'ReverseRotate',
                degrees: count
            });

        
        return handlerInput.responseBuilder
            .speak("Rotating the knob " + count + " degrees counter-clockwise")
            .reprompt("awaiting command")
            .addDirective(directive)
            .getResponse();
    }
};


const RotateIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'RotateIntent';
    },
    handle: function (handlerInput) {
        const request = handlerInput.requestEnvelope;
        const count = Alexa.getSlotValue(request, 'count');

        const attributesManager = handlerInput.attributesManager;
        const endpointId = attributesManager.getSessionAttributes().endpointId || [];
        Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);

        // Construct the directive with the payload containing the move parameters
        const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
            {
                type: 'Rotate',
                degrees: count
            });

        
        return handlerInput.responseBuilder
            .speak("Rotating the knob " + count + " degrees clockwise")
            .reprompt("awaiting command")
            .addDirective(directive)
            .getResponse();
    }
};

const CalibrateIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'CalibrateIntent';
    },
    handle: function (handlerInput) {
        const request = handlerInput.requestEnvelope;
        const position = Alexa.getSlotValue(request, 'position');

        const attributesManager = handlerInput.attributesManager;
        const endpointId = attributesManager.getSessionAttributes().endpointId || [];
        Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);

        // Construct the directive with the payload containing the move parameters
        const directive = Util.build(endpointId, NAMESPACE, NAME_CONTROL,
            {
                type: 'Calibrate',
                position: position
            });

        
        return handlerInput.responseBuilder
            .speak("Calibrting the current position as " + position + " o'clock")
            .reprompt("awaiting command")
            .addDirective(directive)
            .getResponse();
    }
};


// The SkillBuilder acts as the entry point for your skill, routing all request and response
// payloads to the handlers above. Make sure any new handlers or interceptors you've
// defined are included below. The order matters - they're processed top to bottom.
exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        StoveOffTimerIntentHandler,
        MoveIntentHandler,
        RotateIntentHandler,
        ReverseRotateIntentHandler,
        CalibrateIntentHandler,
        Common.HelpIntentHandler,
        Common.CancelAndStopIntentHandler,
        Common.SessionEndedRequestHandler,
        Common.IntentReflectorHandler, // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers
    )
    .addRequestInterceptors(Common.RequestInterceptor)
    .addErrorHandlers(
        Common.ErrorHandler,
    )
    .lambda();

Credits

Smartified Things

Smartified Things

1 project • 1 follower

Comments