Jon Hobbs
Published © GPL3+

Indoor Weather Node

Here's a spin on the venerable temperature/humidity logging module. Our version is an IoT utilizing a Particle Photon and a DHT22.

AdvancedFull instructions provided8 hours1,610
Indoor Weather Node

Things used in this project

Hardware components

Photon
Particle Photon
×1
DHT22 Temperature Sensor
×1
DC Power Plug Supply Adapter Metal Jack Socket
×1
Project Box
×1
Dupont Housing and Pins
×1
Wire
×1
Power Supply
×1
Custom fabricated PCB
OSH Park Custom fabricated PCB
×1

Software apps and online services

Particle Console
Particle Build - IDE
AWS RDS
Amazon Web Services AWS RDS
AWS Lambda
Amazon Web Services AWS Lambda
AWS Elastic Beanstalk
Amazon Web Services AWS Elastic Beanstalk
AWS API Gateway
Amazon Web Services AWS API Gateway
IFTTT Particle Chanel
Particle iPhone App
MySQL Workbench

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Drill (generic)
Wire Stripper (generic)

Story

Read more

Custom parts and enclosures

Web Page

IFTTT Recipe

Configure the Particle Channel, configure the SMS channel, and you're good to go

Schematics

Breadboard Diagram

Schematic

Eagle Schematic

Photon was included during design. Once PCB design was complete, Photon was replaced by relevant male headers. This kept PCB size down, which made it less expensive to manufacture.

Eagle Board File

Photon was included during design. Once PCB design was complete, Photon was replaced by relevant male headers. This kept PCB size down, which made it less expensive to manufacture.

Code

Photon Sketch

Arduino
This is the sketch that is flashed onto the Photon
// This #include statement was automatically added by the Particle IDE.
#include "DHT.h"

//
// Copyright (c) 2016 Jon Hobbs
//
// Built on top of other's hard work, Espcially Nic Jansma
//   Original sketch Copyright (c) 2016 Nic Jansma, http://nicj.net
//      https://github.com/nicjansma/dht-logger
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// *****************
//
// Key changes from Nic's sketch:
//      * Configured for my device
//      * Removed functionality for posting data to Adafruit, ThingSpeak, and generic HTTP
//      * Added alarm functionality
//      * Added zero'd out publish if sensor reading fails
//      * Added funtion and Particle variable to handle web hook response event and spark\status events
//
// ****************
//
// Includes
//
#include "application.h"
#include <string>

//
// Configuration
//

// device & event name
#define DEVICE_NAME "Weather_Node_1"
#define PARTICLE_EVENT_NAME "dht-logger-log"

// sensor type: [DHT11, DHT22, DHT21, AM2301]
#define DHTTYPE DHT11

// which digital pin for the DHT
#define DHTPIN D3

// which digital pin for the Photon/Spark Core/Electron LED
#define LEDPIN D7

// whether to use Farenheit instead of Celsius
#define USE_FARENHEIT 1

// min/max values (sanity checks)
#define MIN_TEMPERATURE -30
#define MAX_TEMPERATURE 120

#define MIN_HUMIDITY 0
#define MAX_HUMIDITY 100

// sensor sending interval (seconds)
#define SEND_INTERVAL 900  // 15 minutes


// Particle alarms
#define PARTICLE_TEMP_HIGH_ALARM 1  // enables high temp alarm
#define PARTICLE_TEMP_HIGH_ALARM_NAME "High-Temp-Alarm"
#define PARTICLE_TEMP_HIGH_ALARM_THRESHOLD 80  //alarm event will be sent if temp > 80

//
// Locals
//
TCPClient tcpClient;

DHT dht(DHTPIN, DHTTYPE);

float humidity;
float temperature;
float heatIndex;

char humidityString[10];
char temperatureString[10];
char heatIndexString[10];
char respEvent[100];

int failed = 0;

// last time since we sent sensor readings
int lastUpdate = 0;

// for Particle.publish payloads
char payload[1024];

/**
 * Handle web hook to AWS API
 *   This function is fired every time the AWS web hook publishes
 *   a response event.  If that response is not a 'success'
 *   message, then republish the current data, which will re-trigger
 *   the web hook.
 */
void myHandler(const char *event, const char *data) {
  // Handle the integration response
    strcpy(respEvent, event);  //converts the event name to a string
    
  if (strstr(event,"hook-response/dht-logger-log") != NULL)  //looks for a specific web hook event
  {
      if (strstr(data, "Success") == NULL)  //web hook event data will contain "Success" if successful
      {                                     //so if strstr is null, "Success" was not in the data
        sprintf(payload,
            "{\"device\":\"%s\",\"temperature\":%.2f,\"humidity\":%.2f,\"heatIndex\":%.2f}",
            DEVICE_NAME,
            temperature,
            humidity,
            heatIndex);
        Particle.publish(PARTICLE_EVENT_NAME, payload, 60, PRIVATE);  //re-publish current data
      }
      else
      {
          Particle.publish("AWS-API-Webhook-Status", "Success", 60, PRIVATE);  //web hook was successful so publish status as an event
      }
    }
}

/**
 * Setup
 */
void setup() {
    pinMode(LEDPIN, OUTPUT);
    digitalWrite(LEDPIN, HIGH);

    // configure Particle variables - float isn't accepted, so we have to use string versions
    // these are the variables that are exposed to the Particle cloud
    Particle.variable("temperature", &temperatureString[0], STRING);
    Particle.variable("humidity", &humidityString[0], STRING);
    Particle.variable("heatIndex", &heatIndexString[0], STRING);
    Particle.variable("status", &failed, INT);
    Particle.variable("respEvent", &respEvent[0], STRING);  //event name - for troubleshooting

    // start the DHT sensor
    dht.begin();

    // startup event
    Particle.subscribe("hook-response/dht-logger-log", myHandler, MY_DEVICES);  //subscribe to web hook response event
    Particle.subscribe("spark/status", myHandler, MY_DEVICES);  //subscribe to status events.  
    sprintf(payload,
            "{\"device\":\"%s\",\"state\":\"starting\"}",
            DEVICE_NAME);

    Particle.publish(PARTICLE_EVENT_NAME, payload, 60, PRIVATE);  //publish a "starting up" event

    // run the first measurement
    loop();
}

/**
 * Event loop
 */
void loop() {
    int now = Time.now();

    // only run every SEND_INTERVAL seconds
    if (now - lastUpdate < SEND_INTERVAL) {
        return;
    }

    // turn on LED when updating
    digitalWrite(LEDPIN, HIGH);
    lastUpdate = now;
    failed = 0;

    // read humidity and temperature values
    humidity = dht.readHumidity();
    temperature = dht.readTemperature(USE_FARENHEIT);

    if (temperature == NAN
        || humidity == NAN
        || temperature > MAX_TEMPERATURE
        || temperature < MIN_TEMPERATURE
        || humidity > MAX_HUMIDITY
        || humidity < MIN_HUMIDITY) {
        // if any sensor failed, publish a zero'd out event.
        failed = 1;
        sprintf(payload,
            "{\"device\":\"%s\",\"temperature\":%.2f,\"humidity\":%.2f,\"heatIndex\":%.2f}",
            DEVICE_NAME,
            0.0,
            0.0,
            0.0);
        //#if PARTICLE_EVENT
        Particle.publish(PARTICLE_EVENT_NAME, payload, 60, PRIVATE);
        //#endif

    } else { //we successfully retrieved valid data from the sensor, so publish it!
        failed = 0;

        // calculate the heat index
        heatIndex = dht.computeHeatIndex(temperature, humidity, USE_FARENHEIT);

        // convert floats to strings for Particle variables
        sprintf(temperatureString, "%.2f", temperature);
        sprintf(humidityString, "%.2f", humidity);
        sprintf(heatIndexString, "%.2f", heatIndex);

        sprintf(payload,
            "{\"device\":\"%s\",\"temperature\":%.2f,\"humidity\":%.2f,\"heatIndex\":%.2f}",
            DEVICE_NAME,
            temperature,
            humidity,
            heatIndex);

        //#if PARTICLE_EVENT
            Particle.publish(PARTICLE_EVENT_NAME, payload, 60, PRIVATE);
        //#endif

        #if PARTICLE_TEMP_HIGH_ALARM  //if alarm is enabled, then examine the threshold and publish alarm event if warrented.
            if (temperature > PARTICLE_TEMP_HIGH_ALARM_THRESHOLD)
            {
                Particle.publish(PARTICLE_TEMP_HIGH_ALARM_NAME, payload, 60, PRIVATE);
            }
        #endif
    }

    // done updating
    digitalWrite(LEDPIN, LOW);
}

Database Schema Create Statement

SQL
The SQL statement that creates our database/schema in RDS
CREATE DATABASE `WeatherNodeLog` /*!40100 DEFAULT CHARACTER SET latin1 */;

Table Create Statements

SQL
The SQL statements that create the 2 tables we use to store our log data.
CREATE TABLE `LocalNodeLog` (
  `LocalNodeLogId` int(11) NOT NULL AUTO_INCREMENT,
  `NodeMAC` varchar(20) NOT NULL,
  `LocalNodeTemp` float DEFAULT NULL,
  `LocalNodeHumidity` float DEFAULT NULL,
  `LogTimeStamp` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`LocalNodeLogId`)
) ENGINE=InnoDB AUTO_INCREMENT=15966 DEFAULT CHARSET=latin1;


CREATE TABLE `LocalNode` (
  `LocalNodeId` int(11) NOT NULL AUTO_INCREMENT,
  `LocalNodeMAC` varchar(20) DEFAULT NULL,
  `LocalNodeLocation` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`LocalNodeId`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=latin1;

Lambda Function

JavaScript
This is the Node.JS code that receives data from Particle cloud web hook and inserts into a table in a MySQL database. Note that things like connect strings, usernames, passwords and other security-related bits of code of been masked to protect the innocent.
var mac;
var temp;
var humid;
var payload;

console.log('Loading');

exports.handler = function (event, context, callback) {

    if (event !== null) 
    {
        console.log('event = ' + JSON.stringify(event));
        mac = event.MAC;
        temp = event.Temp;
        humid = event.Humid;
    }
    else 
    {
        console.log('No event object');
    }
    var mysql = require('mysql');
    var connection = mysql.createConnection({
        host: 'xxx.xxxxxxxxx.xxxxxxx.rds.amazonaws.com',
        user: '<user name>',
        password: '<Password>',
        database: 'Weather'
    });
    connection.connect();

    var qry = "INSERT INTO Weather.LocalNodeLog (NodeMAC, LocalNodeTemp, LocalNodeHumidity) VALUES ('" + mac + "','" + temp + "','" + humid + "');"
    console.log(qry);
    connection.query(qry, function (err, rows, fields) {
        if (!err) 
        {
            console.log('Return is: ', JSON.stringify(rows));
            output = JSON.stringify(rows);
            context.done(null, 'Log Node Successful');  // SUCCESS with message
            callback(null, output);
        }
        else
            console.log('Error while performing Query.');
        context.done("err", "query failure");
    });

    connection.end();
    
};

Credits

Jon Hobbs

Jon Hobbs

9 projects • 27 followers
Just a Minnesota kid hanging out in Kansas
Thanks to Nic Jansma.

Comments