tnunnster
Published © GPL3+

"Alexa: Are My Clothes Dry?"

With Arduino Yun and an accelerometer, monitor clothes dryer vibration, respond to status inquiries, and alert via SMS when the dryer stops.

IntermediateWork in progress4 hours5,950
"Alexa: Are My Clothes Dry?"

Things used in this project

Hardware components

Arduino Yun
Arduino Yun
×1
Parallax MEMSIC 2125 Dual-axis Accelerometer
×1
Echo Dot
Amazon Alexa Echo Dot
×1
Solderless Breadboard Half Size
Solderless Breadboard Half Size
I prefer red
×1
Resistor 10k ohm
Resistor 10k ohm
×1
Jumper wires (generic)
Jumper wires (generic)
×1
Pushbutton switch 12mm
SparkFun Pushbutton switch 12mm
×1
LED (generic)
LED (generic)
Again, I prefer red
×1

Software apps and online services

Arduino IDE
Arduino IDE
Amazon Alexa service
IFTTT Amazon Alexa service
Alexa Voice Service
Amazon Alexa Alexa Voice Service
Cayenne
myDevices Cayenne
ThingSpeak API
ThingSpeak API

Story

Read more

Schematics

Dryer Alert wiring

Hook up a MEMSIC 2125 accelerometer to Arduino Yun

Code

Version 1.0: Dryer Alert Arduino Code using Cayenne API to send SMS

Arduino
The code monitors dryer vibration and uses Cayenne to send SMS when the dryer vibration stops.
// Include statements:
#include <Bridge.h>

//#define CAYENNE_DEBUG       // Uncomment to show debug messages
#define CAYENNE_PRINT Serial  // Comment this out to disable prints and save space
#include <CayenneYun.h>

// Cayenne authentication token. This should be obtained from the Cayenne Dashboard.
char token[] = "xxxxxxxxxxxxx";

// Adjust these to meet your needs
const int buttonPin = 2;            // pin number of the pushbutton
const int xPin = 3;                 // pin number of the X output of the accelerometer
const int yPin = 4;                 // pin number of the Y output of the accelerometer
const int ledPin = 13;              // pin number of the LED
const int waitTime = .1;            // wait time in minutes
const float sensitivityX = 0.002;   // sensitivity of X axis in percent change
const float sensitivityY = 0.002;   // sensitivity of Y axis in percent change

// Variables:
boolean lastButtonState = LOW;
boolean currentButtonState = LOW;
boolean ledState = LOW;
int counter = 0;
float lastPulseX = 0;
float lastPulseY = 0;

// Variables to contain the resulting accelerations
  int accelerationX, accelerationY;

// Initial setup
void setup() {
  // Initialize serial communications:
  Serial.begin(9600);
  
  // Initiate Cayenne communications
  Cayenne.begin(token);
  
  // Initialize the pins:
  pinMode(xPin, INPUT);
  pinMode(yPin, INPUT);
  pinMode(buttonPin, INPUT);
  pinMode(ledPin, OUTPUT);
  
  Bridge.begin();
}

// Debounce function for the pushbutton
boolean debounce(boolean last)
{
  boolean current = digitalRead(buttonPin);
  if(last != current)
  {
    delay(5);
    current = digitalRead(buttonPin);
  }
  return current;
}

// Main loop
void loop()
{
  Cayenne.run();
  delay (5);
     
  // Set the LED
  currentButtonState = debounce(lastButtonState);
  if(lastButtonState == LOW && currentButtonState == HIGH)
  {
    ledState = !ledState;
  }
  lastButtonState = currentButtonState;
  digitalWrite(ledPin, ledState);
  
  // If the led is on
  if (ledState == HIGH)
  { 
    // If the counter is less than the wait time.
    // I multiplied the waitTime by 3000 to account for timing.
    // You may need to adjust this value to get more accurate timing.
    // if (counter < (waitTime * 3000))
    if (counter < (10))  // For testing only
    {
     
      // Declare variables to read the pulse widths, change, and percentage change:
      float pulseX, pulseY, changeX, changeY, percentX, percentY;
    
      // Read pulse from x- and y-axes:
      pulseX = pulseIn(xPin,HIGH);  
      pulseY = pulseIn(yPin,HIGH);
      
      // Convert the pulse width into acceleration
      // accelerationX and accelerationY are in milli-g's:
      // Earth's gravity is 1000 milli-g's, or 1g.
      accelerationX = abs((pulseX / 10) - 500) * 8;
      accelerationY = abs((pulseY / 10) - 500) * 8;
      // print the acceleration
      Serial.print(accelerationX);
      // print a tab character:
      Serial.print("\t");
      Serial.print(accelerationY);
      Serial.println();    
          
      // Find the change in the pulse:
      changeX = lastPulseX - pulseX;
      changeY = lastPulseY - pulseY;
    
      // Calculate the percentage change using absolute values:
      percentX = abs(changeX / lastPulseX);
      percentY = abs(changeY / lastPulseY);
    
      // If the percentage change is less than the sensitivity (i.e. no movement detected)
      if (percentX < sensitivityX && percentY < sensitivityY)
      {      
        // Increase the counter
        counter++;
      }
      // Else if movement is detected
      else
      { 
        // Reset the counter
        counter = 0;
      }

      // Print the counter
      Serial.print(counter);
      Serial.println(); 
      delay(1000);
          
      // Set the last pulse equal to the current pulse
      lastPulseX = pulseX;
      lastPulseY = pulseY;
    }
    else
    {
      // Reset the counter
      // Comment out to end the loop
      counter = 0;
      ledState = LOW;
    }
    //  Serial.print("Life is good.");
    //  Serial.println(); 
    // Serial.end();
    //ledState = LOW;
  }
}

Version 2.0 Update - Add ThingSpeak API configuration

Arduino
This version adds the necessary library and code to allow monitoring of dryer status via a public ThingSpeak channel.
// ThingSpeak - Version: Latest 
#include <ThingSpeak.h>

// Include statements:
#include <Bridge.h>

//#define CAYENNE_DEBUG         // Uncomment to show debug messages
#define CAYENNE_PRINT Serial  // Comment this out to disable prints and save space
#include <CayenneYun.h>

// Cayenne authentication token. This should be obtained from the Cayenne Dashboard.
char token[] = "u19xjr2vmj";

// ThingSpeak example
#include "YunClient.h"
YunClient client;

unsigned long myChannelNumber = 177778;
const char * myWriteAPIKey = "FCWFJPEHWXSS26U7";

// Adjust these to meet your needs
const int buttonPin = 2;            // pin number of the pushbutton
const int xPin = 3;                 // pin number of the X output of the accelerometer
const int yPin = 4;                 // pin number of the Y output of the accelerometer
const int ledPin = 13;              // pin number of the LED
const int waitTime = .1;             // wait time in minutes
const float sensitivityX = 0.002;   // sensitivity of X axis in percent change
const float sensitivityY = 0.002;   // sensitivity of Y axis in percent change

// Variables:
boolean lastButtonState = LOW;
boolean currentButtonState = LOW;
boolean ledState = LOW;
int counter = 0;
float lastPulseX = 0;
float lastPulseY = 0;

// (TN) variables to contain the resulting accelerations
int accelerationX, accelerationY;


// Initial setup
void setup() {
  // (TN) initialize serial communications:
  Serial.begin(9600);
  
  // (TN) Initiate Cayenne communications
  Cayenne.begin(token);

  // initialize the pins:
  pinMode(xPin, INPUT);
  pinMode(yPin, INPUT);
  pinMode(buttonPin, INPUT);
  pinMode(ledPin, OUTPUT);
  
  Bridge.begin();

  // ThingSpeak
  ThingSpeak.begin(client);

}

// Debounce function for the pushbutton
boolean debounce(boolean last)
{
  boolean current = digitalRead(buttonPin);
  if(last != current)
  {
    delay(5);
    current = digitalRead(buttonPin);
  }
  return current;
}

// Main loop
void loop()
{
  Cayenne.run();
  delay (5);
     
  // Set the LED
  currentButtonState = debounce(lastButtonState);
  if(lastButtonState == LOW && currentButtonState == HIGH)
  {
    ledState = !ledState;
  }
  lastButtonState = currentButtonState;
  digitalWrite(ledPin, ledState);
  
  // If the led is on
  if (ledState == HIGH)
  { 
    // If the counter is less than the wait time.
    // I multiplied the waitTime by 3000 to account for timing.
    // You may need to adjust this value to get more accurate timing.
    // if (counter < (waitTime * 3000))
    if (counter < (3))  // For testing only
    {
 
      // Declare variables to read the pulse widths, change, and percentage change:
      float pulseX, pulseY, changeX, changeY, percentX, percentY;

      // Read pulse from x- and y-axes:
      pulseX = pulseIn(xPin,HIGH);  
      pulseY = pulseIn(yPin,HIGH);
      
      // [TN] convert the pulse width into acceleration
      // accelerationX and accelerationY are in milli-g's:
      // earth's gravity is 1000 milli-g's, or 1g.
      accelerationX = abs((pulseX / 10) - 500) * 8;
      accelerationY = abs((pulseY / 10) - 500) * 8;
 
      // print the acceleration
      // Serial.print(accelerationX);
      // print a tab character:
      // Serial.print("\t");
      // Serial.print(accelerationY);
      // Serial.println();   
          
      // Find the change in the pulse:
      changeX = lastPulseX - pulseX;
      changeY = lastPulseY - pulseY;
    
      // Calculate the percentage change using absolute values:
      percentX = abs(changeX / lastPulseX) * 100;
      percentY = abs(changeY / lastPulseY) * 100;

      // print the percent change
      Serial.print(percentX);
      // print a tab character:
      Serial.print("\t");
      Serial.print(percentY);
      Serial.println();   
        
      ThingSpeak.setField(1, ledState);
      ThingSpeak.setField(2, percentX);
      ThingSpeak.setField(3, percentY);

      // Write the fields all at once.
      ThingSpeak.writeFields(myChannelNumber, myWriteAPIKey);
      delay(20000); // ThingSpeak will only accept updates every 15 seconds.  
         
      // If the percentage change is less than the sensitivity (i.e. no movement detected)
      if ((percentX / 100) < sensitivityX && (percentY /100) < sensitivityY)
      {      
        // Increase the counter
        counter++;
      }
      // Else if movement is detected
      else
      { 
        // Reset the counter
        counter = 0;
      }

      // [TN] print the counter
      Serial.print(counter);
      Serial.println(); 
      delay(1000);
          
      // Set the last pulse equal to the current pulse
      lastPulseX = pulseX;
      lastPulseY = pulseY;
    }
    else
    {
      // Reset the counter
      // [TN] Comment out to end the loop
      counter = 0;
      ledState = LOW;
      ThingSpeak.writeField(myChannelNumber, 1, ledState, myWriteAPIKey);      
    }
    //  Serial.print("Life is good.");
    //  Serial.println(); 
    // Serial.end();
    //ledState = LOW;
  }
}

Dryer Status Skill Intents

JSON
Basic intents for configuring the Alexa Skill
{
 "intents": [
   {
     "intent": "DryerStatusIntent"
   },
   {
     "intent": "AMAZON.HelpIntent"
   },
   {
     "intent": "AMAZON.CancelIntent"
   }, 
   {
     "intent": "AMAZON.StopIntent"
   }
 ]
}

Dryer Status Skill Utterances

snippets
Utterances are needed for the Dryer Status intent to trigger the AWS Lambda service
DryerStatusIntent are my clothes dry
DryerStatusIntent check the dryer status
DryerStatusIntent what's the dryer status
DryerStatusIntent what is the dryer status
DryerStatusIntent is the dryer on
DryerStatusIntent is the dryer busy

myDryerStatus Lamdba Function

Python
The Lambda function instructs Alexa how to interpret utterances from the Dryer Alert skill, how to check dryer status on ThingSpeak, and how to provide voice response based on dryer status.
"""
This skill returns data from a ThingSpeak channel.  In my case, we're checking the status of my dryer.

Ask Alexa "are my clothes dry" to get the current status.

February 2018

Shout out and full credit to kayakpete
https://www.hackster.io/kayakpete/amazon-echo-alexa-thingspeak-data-checker-8677c0

"""

from __future__ import print_function
import urllib

# Change these elements to point to your data
channel = 177778
field = 1
#

link = "https://api.thingspeak.com/channels/" + \
    str(channel) + \
    "/fields/" + \
    str(field) + \
    "/last"

def lambda_handler(event, context):
    """ Route the incoming request based on type (LaunchRequest, IntentRequest,
    etc.) The JSON body of the request is provided in the event parameter.
    """
    print("event.session.application.applicationId=" +
          event['session']['application']['applicationId'])

    if event['session']['new']:
        on_session_started({'requestId': event['request']['requestId']},
                           event['session'])

    if event['request']['type'] == "LaunchRequest":
        return on_launch(event['request'], event['session'])
    elif event['request']['type'] == "IntentRequest":
        return on_intent(event['request'], event['session'])
    elif event['request']['type'] == "SessionEndedRequest":
        return on_session_ended(event['request'], event['session'])


def on_session_started(session_started_request, session):
    """ Called when the session starts """

    print("on_session_started requestId=" + session_started_request['requestId']
          + ", sessionId=" + session['sessionId'])


def on_launch(launch_request, session):
    """ Called when the user launches the skill without specifying what they
    want
    """

    print("on_launch requestId=" + launch_request['requestId'] +
          ", sessionId=" + session['sessionId'])
    # Dispatch to your skill's launch
    return get_welcome_response()


def on_intent(intent_request, session):
    """ Called when the user specifies an intent for this skill """

    print("on_intent requestId=" + intent_request['requestId'] +
          ", sessionId=" + session['sessionId'])

    intent = intent_request['intent']
    intent_name = intent_request['intent']['name']

    # Dispatch to your skill's intent handlers
    if intent_name == "DryerStatusIntent":
        return check_dryer_status(intent, session, link)
    elif intent_name == "AMAZON.HelpIntent":
        return get_welcome_response()
    elif intent_name == "AMAZON.CancelIntent" or intent_name == "AMAZON.StopIntent":
        return handle_session_end_request()
    else:
        raise ValueError("Invalid intent")


def on_session_ended(session_ended_request, session):
    """ Called when the user ends the session.

    Is not called when the skill returns should_end_session=true
    """
    print("on_session_ended requestId=" + session_ended_request['requestId'] +
          ", sessionId=" + session['sessionId'])

# --------------- Functions that control the skill's behavior ------------------

def get_welcome_response():

    session_attributes = {}
    card_title = "Welcome"
    speech_output = "Welcome to Dryer Alert. " \
                    "I can tell you the status of the dryer from Thing Speak. " \
                    "Just ask me to check the dryer status"
    # If the user either does not reply to the welcome message or says something
    # that is not understood, they will be prompted again with this text.
    reprompt_text = "Ask me, what is my dryer status. "
    should_end_session = False
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, reprompt_text, should_end_session))

def handle_session_end_request():
    card_title = "Session Ended"
    speech_output = "I hope the current status suits your needs. " \
                    "Party on, dude! "
    # Setting this to true ends the session and exits the skill.
    should_end_session = True
    return build_response({}, build_speechlet_response(
        card_title, speech_output, None, should_end_session))

# Check the Dryer Status
def check_dryer_status(intent, session, link):
    session_attributes = {}
    reprompt_text = None
    should_end_session = False
    
    f = urllib.urlopen(link) # Get your data
    result = f.read()
    
    speech_output = "The current dryer status is " + \
                    result + \
                    "."
                    
    ## Comment on the awesomeness
    if result == "1":
        comment = " The dryer is still on, dude."
    else:
        comment = " The dryer is off, dude."
       
    speech_output = speech_output + comment
    
    return build_response(session_attributes, build_speechlet_response(
        intent['name'], speech_output, reprompt_text, should_end_session))

# --------------- Helpers that build all of the responses ----------------------

def build_speechlet_response(title, output, reprompt_text, should_end_session):
    return {
        'outputSpeech': {
            'type': 'PlainText',
            'text': output
        },
        'card': {
            'type': 'Simple',
            'title': 'SessionSpeechlet - ' + title,
            'content': 'SessionSpeechlet - ' + output
        },
        'reprompt': {
            'outputSpeech': {
                'type': 'PlainText',
                'text': reprompt_text
            }
        },
        'shouldEndSession': should_end_session
    }


def build_response(session_attributes, speechlet_response):
    return {
        'version': '1.0',
        'sessionAttributes': session_attributes,
        'response': speechlet_response
    }

Credits

tnunnster

tnunnster

3 projects • 5 followers
Little Lebowski Urban Achiever and part-time Abider. Very appreciative of the community and the open network of project ideas.
Thanks to MakerBee and Kayak Pete.

Comments