Atul
Published © Apache-2.0

Smart Greenhouse: The future of agriculture

Smart Greenhouse is a self regulating, micro-climate controlled environment for optimal plant growth.

IntermediateShowcase (no instructions)41,649
Smart Greenhouse: The future of agriculture

Things used in this project

Hardware components

Intel Edison and Arduino breakout kit
×2
Arduino Proto Screw Shield by Itead
×2
TMP36 - Temperature Sensor
×1
Arduino compatible Mini Luminance Sensor
×1
Humidity Sensor Breakout - HIH-4030
×1
Addressable RGB LED Pixel Strip 5 Meter roll
×1
SainSmart 8-Channel Relay Module
×1
Cutequeen Trading 2PCS White 5050 48SMD 48-SMD LED Panel Light
×1
12V Coil 8 Pin DPDT Green LED Indicator Power Relay
×1
SMAKN® IRF520 MOS FET Driver Module for Arduino
×1
12v Fan-Tastic 01100WH Endless Breeze Stand alone Fan
×1
High Speed 12-Volt Linear Actuator - 4" Stroke, 65 mm/s, 45 LB Load Rating
×1
SparkFun Block for Intel® Edison - Battery
×1
SparkFun Block for Intel® Edison - ADC
×1
Soil Moisture Meter Testing Module
×1
Universal 24V 2A Output Regulated Switching Power Suppl
×1
AVAWO® 12V 10A DC 120W Switching Power Supply Transformer for LED CCTV
×1
uxcell AC110/220V DC5V 10A 50W LED Strip Light Switching Power Supply Adapter
×1
8x12 Vitavia Jupiter Greenhouse
×1

Software apps and online services

AWS IoT
Amazon Web Services AWS IoT
AWS Lambda
Amazon Web Services AWS Lambda
AWS DynamoDB
Amazon Web Services AWS DynamoDB
AWS EC2
Amazon Web Services AWS EC2
AWS IAM
Amazon Web Services AWS IAM
AWS S3
Amazon Web Services AWS S3

Story

Read more

Code

Actuator Node

JavaScript
Code that runs on micro-controller board connected to all the actuators. This code listens for commands on an MQTT channel connected to AWS IOT service and executes this command by interpreting the message and carrying out actuator actions.
/* Actuator Node Code for IOT Greenhouse
   09.21.2015
*/

var mraa = require("mraa"); //require mraa
var mqtt = require('mqtt');
var fs = require('fs');

console.log('MRAA Version: ' + mraa.getVersion()); //write the mraa version to the Intel XDK console
 

var FanPin = new mraa.Gpio(1);
var LightPwmPin = new mraa.Pwm(3); //3, 5, 6, 9
var MisterPin = new mraa.Gpio(2);
var ActuatorPin = new mraa.Gpio(7);

var SPI = new mraa.Spi(0);
SPI.mode(0);
SPI.lsbmode(true);
SPI.frequency(500000);
SPI.bitPerWord(8);

var LEDStrip = require('./ledstrip.js');
var leds = new LEDStrip(SPI, 153);
leds.setup();

LightPwmPin.enable(true);
LightPwmPin.period_us(3000); //2000
 

var value = 0;
var CurrentLightValue = 0.0;
var PreviousLightValue = 0.0;

//OverheadLightPin.dir(mraa.DIR_OUT);
FanPin.dir(mraa.DIR_OUT);
MisterPin.dir(mraa.DIR_OUT);
ActuatorPin.dir(mraa.DIR_OUT);

//OverheadLightPin.write(1);
 

/* Set output power-on states */
writeFan(0);
writeMister(0);
writeVent(0);
writeOverheadLight('0.0');
leds.fill([0,0,0]);

function writeOverheadLight(RequestedLightValue) {
  console.log("Setting Overhead Light intensity " + RequestedLightValue);

  var iv = setInterval(function () {
    if (CurrentLightValue >= RequestedLightValue) {
      CurrentLightValue -= 0.01;
    } else if (CurrentLightValue < RequestedLightValue) {
      CurrentLightValue += 0.01;
    } else {
      CurrentLightValue = 0.0;
    }

    CurrentLightValue = +CurrentLightValue.toFixed(2);

    //console.log(CurrentLightValue)
    LightPwmPin.write(CurrentLightValue);

    if (CurrentLightValue == RequestedLightValue) clearInterval(iv);

  }, 10);
}

function readOverheadLight() {
  return parseFloat(LightPwmPin.read());
}

function writeLEDStrip(red, green, blue) {
  //console.log("red:" + red);
  //console.log("green:" + green);
  //console.log("blue:" + blue);
  //process.nextTick(function() {;
  //setImmediate(function() {
    leds.fill([parseInt(red), parseInt(green), parseInt(blue)]);
  //});
}

function readLEDStrip() {
  var resp = {};
  resp.Red = 0.0;
  resp.Green = 0.0;
  resp.Blue = 0.0;
  return resp;
}

function writeFan(Value) {
  if (Value == '1') { Value = '0' } else { Value = '1'; }
  FanPin.write(parseInt(Value));
}

function readFan(){
  var Value = FanPin.read();  
  if (Value == '1') { Value = '0' } else { Value = '1'; }
  return parseInt(Value);
}

function writeMister(Value) {
  if (Value == '1') { Value = '0' } else { Value = '1'; }
  MisterPin.write(parseInt(Value));
}

function readMister(){
  var Value =  MisterPin.read();
  if (Value == '1') { Value = '0' } else { Value = '1'; }
  return parseInt(Value);
}

function writeVent(Value) {
  //console.log(parseInt(Value));
  if (Value == '1') { Value = '0' } else { Value = '1'; }
  ActuatorPin.write(parseInt(Value));
}

function readVent(){
  var Value = ActuatorPin.read();
  if (Value == '1') { Value = '0' } else { Value = '1'; }
  return parseInt(Value);
}

// ssl cert config
var icebreakerCirtsDir = __dirname + '/certs/icebreaker';

var options = {
  key: fs.readFileSync(icebreakerCirtsDir + '/iot-greenhouse-private.crt'),
  cert: fs.readFileSync(icebreakerCirtsDir + '/iot-greenhouse.pem'),
  ca: [fs.readFileSync(icebreakerCirtsDir + '/rootCA.pem')],
  requestCert: true,
  rejectUnauthorized: true,
  port: 8883
};

console.log("Connecting to AWS MQTT server...");

var client  = mqtt.connect('mqtts://data.iot.us-east-1.amazonaws.com',options);
client.on('connect', function () {
  console.log("Connected to Icebreaker");

  var actuatorTopicPrefix = 'IOTGreenhouse/Actuators/';
  var actuator_topics = ["Lights/OverheadLamps/Lamp1", "Lights/LEDLamps/Strip1", "Fans/VentilationFans/Fan1",
    "Sprinklers/Misters/Mister1", "Windows/Vents/Vent1", "Windows/Vents/Vent2"];

  var mqttSubscriptionTopics = {};
  actuator_topics.map(function(topic) { mqttSubscriptionTopics[actuatorTopicPrefix + topic] = 1; });

  client.subscribe(mqttSubscriptionTopics);
});

client.on('message', subscribeCallback);

client.on('error', function (err) {
  console.log('Error Connecting to IceBreaker ' + err);
});

var dataSource = 'IOTGreenhouse';

function subscribeCallback(mqttTopic, mqttMessage) {
  var topicParts = mqttTopic.split('/');
  if(topicParts.length > 3) {
    var partIndex = 0;
    if(topicParts[partIndex] == dataSource) {
      partIndex++;
      if(topicParts[partIndex] == 'Actuators') {
        partIndex++;
        try {
          var commandObject = JSON.parse(mqttMessage);
          if(commandObject) {
            executeActuatorCommand(topicParts, commandObject, function(responseObject) {
              // Publish response on status channel
              var publishTopic = mqttTopic.replace('/Actuators', '/Actuators/Status');
              client.publish(publishTopic, JSON.stringify(responseObject));
            });
          } else {
            console.log('Received incorrect message ' + mqttMessage);
          }
        } catch (err) {
          console.log("received invalid mqtt JSON message, Error " + err);
        }
      } else {
        console.log('Ignoring invalid MQTT device type ' + topicParts[partIndex]);
      }
    } else {
      console.log('Ignoring message from non supported MQTT source ' + topicParts[partIndex]);
    }
  } else {
    console.log('Received incomplete MQTT topic ' + mqttTopic);
  }
}

function executeActuatorCommand(topicParts, commandObject, callback) {
  console.log('Actuator ' + topicParts[topicParts.length - 1] + ' Command ' + JSON.stringify(commandObject));
  if(!commandObject.state) {
    console.log("Received invalid command" + JSON.stringify(commandObject));
  }
  switch (topicParts[topicParts.length - 1]) {
    case "Lamp1":    		//Overhead light
      executeOverheadLightCommand(commandObject, callback);
      break;
    case "Strip1":		//RGB Pixel Strip
      executeLEDStripCommand(commandObject, callback);
      break;
    case "Fan1":        	//Fan
      executeFanCommand(commandObject, callback);
      break;
    case "Mister1":			//Mister
      executeMisterCommand(commandObject, callback);
      break;
    case "Vent1":			//Window Vent
      executeVentCommand(commandObject, callback);
      break;

  }
}

function executeOverheadLightCommand(commandObject, callback) {
  var command = commandObject.state;
  var intensity = 0.0;
  if(commandObject.state ==='on') {
    intensity = 1.0;
    if(commandObject.intensity) {
      intensity = commandObject.intensity;
    }
  }
  writeOverheadLight(intensity);

  // Read Value after 1 sec
  setTimeout(function() {
    var respIntensity = readOverheadLight();
    var responseObject = { reading_timestamp : new Date().getTime() };
    responseObject.state = respIntensity ? "on" : "off";
    responseObject.intensity = respIntensity;
    callback(responseObject)
  }, 2000);
}

function executeLEDStripCommand(commandObject, callback) {
  var redIntensity = 0;
  var greenIntensity = 0;
  var blueIntensity = 0;
  if (commandObject.state === 'on') {
    if (commandObject.redIntensity) redIntensity = commandObject.redIntensity;
    if (commandObject.greenIntensity) greenIntensity = commandObject.greenIntensity;
    if (commandObject.blueIntensity) blueIntensity = commandObject.blueIntensity;
  }
  writeLEDStrip(redIntensity, greenIntensity, blueIntensity);
  // Read Value after .5 sec
  setTimeout(function() {
    var resp = readLEDStrip();
    var responseObject = { reading_timestamp : new Date().getTime() };
    responseObject.state = resp.Red || resp.Green || resp.Blue ? "on" : "off";
    responseObject.redIntensity = resp.Red;
    responseObject.greenIntensity = resp.Green;
    responseObject.blueIntensity = resp.Blue;
    callback(responseObject);
  }, 500);

}

function executeFanCommand(commandObject, callback) {
  var command = commandObject.state;
  writeFan(commandObject.state ==='on' ? 1 : 0);

  // Read Value after 3 sec
  setTimeout(function() {
    var responseObject = { reading_timestamp : new Date().getTime() };
    responseObject.state = readFan() ? "on" : "off";
    callback(responseObject)
  }, 3000);
}

function executeMisterCommand(commandObject, callback) {
  var command = commandObject.state;
  writeMister(commandObject.state ==='on' ? 1 : 0);

  // Read Value after 2 sec
  setTimeout(function() {
    var responseObject = { reading_timestamp : new Date().getTime() };
    responseObject.state = readMister() ? "on" : "off";
    callback(responseObject)
  }, 2000);
}

function executeVentCommand(commandObject, callback) {
  var command = commandObject.state;
  writeVent(commandObject.state ==='open' ? 1 : 0);

  // Read Value after 5 sec
  setTimeout(function() {
    var responseObject = { reading_timestamp : new Date().getTime() };
    responseObject.state = readVent() ? "open" : "close";
    callback(responseObject)
  }, 10000);
}
 
 
 

process.on('SIGINT', function() {
  var ForceOff = new mraa.Gpio(3);
  ForceOff.dir(mraa.DIR_OUT);
  ForceOff.write(0);
  //LightPwmPin.write(0.0);
  leds.fill([0,0,0]);
  process.exit(0);
});
 
 

Sensor Node

JavaScript
Code that runs on micro-controller board connected to all the sensors. Sensor reading is collected from each connected sensor every second and is posted to AWS IOT service over an MQTT channel.
/*
  IOT Greenhouse Sensor Node Code
*/

var mraa = require('mraa'); //require mraa
console.log('MRAA Version: ' + mraa.getVersion()); //write the mraa version to the Intel XDK console
var mqtt = require('mqtt');
var fs = require('fs');



var myOnboardLed = new mraa.Gpio(13); //LED hooked up to digital pin 13 (or built in pin on Intel Galileo Gen2 as well as Intel Edison)
myOnboardLed.dir(mraa.DIR_OUT);

var LightSensorPin = new mraa.Aio(0);
var HumiditySensorPin = new mraa.Aio(1);
var SoilSensorPin = new mraa.Aio(2);
var TempSensorPin = new mraa.Aio(3);


var B = 3975;
var LoopInterval = null;
var CollectingData = false;

// ssl cert config
var icebreakerCirtsDir = __dirname + '/certs/icebreaker';

var options = {
  key: fs.readFileSync(icebreakerCirtsDir + '/iot-greenhouse-private.crt'),
  cert: fs.readFileSync(icebreakerCirtsDir + '/iot-greenhouse.pem'),
  ca: [fs.readFileSync(icebreakerCirtsDir + '/rootCA.pem')],
  requestCert: true,
  rejectUnauthorized: true,
  port: 8883,
  cleanSession: true,
  //reconnectPeriod: 2000,
  //connectTimeout: 2000,
  protocolId: 'MQIsdp',
  protocolVersion: 3
};

var client  = mqtt.connect('mqtts://data.iot.us-east-1.amazonaws.com',options);

console.log("Connecting to AWS MQTT server...");
client.on('connect', function onConnect(data) {
  console.log("Connected: " + data);

  //only restart the sensor watch if it is not running. This has to do with the setinterval not
  //being cleared correctly on error
  if (CollectingData == false) {
    startSensorWatch(); 
  }
});

//Emitted only when the client can't connect on startup
client.on('error', function (err) {  
  console.log('Error connecting to IceBreaker ' + err); 
});

//Emitted after a disconnection
client.on('offline', function () {
  console.log('IceBreaker connection lost or WiFi down.'); 
  clearInterval(LoopInterval);
});

//Emitted after a closed connection
client.on('close', function () {
  console.log('IceBreaker connection closed.'); 
  clearInterval(LoopInterval);
});

//Emitted after a reconnection
client.on('reconnect', function () {
  console.log('IceBreaker connection restored.'); 
  startSensorWatch();
});

/*
Function: startSensorWatch(client)
Parameters: client - mqtt client communication channel
Description: Read Sensor Data on timer event and send it to AWS IOT
*/
//function startSensorWatch(client) {
function startSensorWatch() {
    'use strict';

    var Sensor1Success = false;
    var Sensor2Success = false;
    var Sensor3Success = false;
    var Sensor4Success = false;

    LoopInterval = setInterval(function () {

        myOnboardLed.write(1);
        CollectingData = true;
        
  
        var LightSensorValue = LightSensorPin.read();

        var HumiditySensorValue = HumiditySensorPin.read();
        var HumiditySensorPercentage = SensorMap(HumiditySensorValue,200,700,0,100);

        var TempSensorValue = TempSensorPin.read();
        var TempSensorMilliVolts = (TempSensorValue * (5000 / 1024)); //for 5v AVREF
        var CentigradeTemp = (TempSensorMilliVolts - 500) / 10;
        var fahrenheit_temperature = (((CentigradeTemp * 9) / 5) + 32).toFixed(2);


        var message = {reading_timestamp:new Date().getTime(), luminosity:LightSensorValue};
        client.publish('IOTGreenhouse/Sensors/Luminance/Sensor1', JSON.stringify(message), {
        }, function() {
          //console.log("luminosity: " + LightSensorValue); 
          Sensor1Success = true;
        }); 

        var message = {reading_timestamp:new Date().getTime(), humidity:HumiditySensorPercentage};
        client.publish('IOTGreenhouse/Sensors/Humidity/Air/Sensor1', JSON.stringify(message), { 
        }, function() {
          console.log("humidity: " + HumiditySensorPercentage); 
          Sensor2Success = true;
        });

        var message = {reading_timestamp:new Date().getTime(), temperature:fahrenheit_temperature};
        client.publish('IOTGreenhouse/Sensors/Temperature/Sensor1', JSON.stringify(message), { 
        }, function () { 
          
          Sensor3Success = true;
        });


        //Only flash the heartbeat indicator if all sensors read correctly
        //if not, leave it on solid
        if (Sensor1Success && Sensor2Success && Sensor3Success) {
        //if (Sensor4Success) {
          myOnboardLed.write(0);

          var Sensor1Success = false;
          var Sensor2Success = false;
          var Sensor3Success = false;
          var Sensor4Success = false;

        }
 

    }, 1000);

}

function SensorMap(x, in_min, in_max, out_min, out_max)
{
  return Math.round((x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min);
}
 

process.on('SIGINT', function() {
    console.log("Received Interrupt, Exiting...");
    clearInterval(LoopInterval);
    client.end();     
    process.exit(0);
});
 

AWS Lambda funtion to Monitor Humidity in the Greenhouse

JavaScript
This AWS Lambda function gets triggered on every humidity reading from the greenhouse. When pre-set conditions are met, actions specified in configuration file are executed in order to monitor humidity in the greenhouse
/**
 * Created by atulbar on 9/25/15.
 */
console.log('Loading function');
var nconf = require('nconf');
var AWS = require('aws-sdk');

//Setup nconf to use (in-order):
// 1. Command-line arguments,
// 2. Environment variables
// 3. A file located at 'path/to/config.json'
nconf.argv().env().file({ file: __dirname + '/air-humidity-monitor-config.json' });
// nconf.argv().env().file({ file: __dirname + '/temperature-monitor-config.json' });

// create IceBreaker Mqtt Client and connect it to the server
var icebreakerMqttClient = require('./icebreaker-mqtt-client');
icebreakerMqttClient.connect();

var docClient = new AWS.DynamoDB.DocumentClient({region: nconf.get('aws_region')});
 

console.log('Settings ' + JSON.stringify(nconf.get(), null, 2) );

var MEASUREMENT = nconf.get('measurement');
var MEASUREMENT_HIGH_THRESHOLD = nconf.get('measurement_high_threshold'); // measurement high threshold
var MEASUREMENT_LOW_THRESHOLD = nconf.get('measurement_low_threshold'); // measurement low threshold
var EVALUATION_DURATION = nconf.get('evaluation_duration'); // duration for evaluation of trigger in seconds
var MEASUREMENT_SENSOR = nconf.get('measurement_sensor');
var HIGH_THRESHOLD_ALARM_ACTIONS = nconf.get('high_threshold_alarm_actions');
var LOW_THRESHOLD_ALARM_ACTIONS = nconf.get('low_threshold_alarm_actions');

var ALARM_TABLE_NAME = nconf.get('alarm_table_name') || 'IOTGreenhouse_alarms';

var dataSource = nconf.get('mode');
var sensorTopicPrefix = dataSource + '/Sensors/';
var sensors = nconf.get('sensors');
var actuators = nconf.get('actuators');

var MEASUREMENT_SENSOR_ID;
if(sensors) {
    MEASUREMENT_SENSOR_ID = sensorTopicPrefix + sensors[MEASUREMENT_SENSOR];
}

exports.handler = function(event, context) {
    // console.log('Received event:', JSON.stringify(event, null, 2));
    if( ! MEASUREMENT_SENSOR_ID ) failWithMessage('No Sensor is configured');
    if( ! event[MEASUREMENT] ) failWithMessage('Incorect event received');

    readCurrentAlarmState(function(err, alarmState) {
        if( err ) failWithMessage('Failed to read alarm state due to error ' + err);

        if( ! alarmState.processing ) { // alarm is off
            if( crossedHighThreshold(event) ) { // crossed threshold for first time
                initializeAlarmProcessing(true, function (err) {
                    if (err) failWithMessage('Failed to initialize alarm processing due to error ' + err);
                    context.succeed();
                });
            } else if( crossedLowThreshold(event) ) { // crossed threshold for first time
                initializeAlarmProcessing(false, function(err) {
                    if(err) failWithMessage('Failed to initialize alarm processing due to error ' + err);
                    context.succeed();
                });
            } else {
                context.succeed();  // threshold not reached
            }
        } else { // alarm was  processing
            if( !(alarmState.highThreshold) && crossedHighThreshold(event) ) { // crossed other threshold
                initializeAlarmProcessing(true, function (err) {
                    if (err) failWithMessage('Failed to initialize alarm processing due to error ' + err);
                    context.succeed();
                });
            } else if( alarmState.highThreshold && crossedLowThreshold(event) ) { // crossed threshold for first time
                initializeAlarmProcessing(false, function(err) {
                    if(err) failWithMessage('Failed to initialize alarm processing due to error ' + err);
                    context.succeed();
                });
            } else if(droppedBackWithinTolerance(event, alarmState)) { // we dropped back within tolerance
                console.log(MEASUREMENT + ' back within tolerance, clearing alarm processing');
                clearAlarmProcessing(function(err){
                    if(err) failWithMessage('Failed to clear alarm state due to error ' + err);
                    context.succeed();
                });
            } else { // we continue to be above threshold
                alarmState.readingCount++;
                updateAlarmProcessing(alarmState, function(err){
                    if(err) failWithMessage('Failed to update alarm state due to error ' + err);
                    console.log('alarm reading count ' + alarmState.readingCount);
                    if( alarmState.readingCount > 2 )  { // at least 3 readings
                        var timeElapsedInSeconds = ( new Date().getTime() - alarmState.firstReadingTime ) / 1000;
                        if( timeElapsedInSeconds >  EVALUATION_DURATION ) {
                            fireAlarm(alarmState, function(err){
                                if(err) console.log('Failed to fire alarm state due to error ' + err);  // do not fail here, we need to clear alarm state
                                clearAlarmProcessing(function(errDb){
                                    if(errDb) failWithMessage('Failed to clear alarm state due to error ' + errDb );
                                    context.succeed();
                                });
                            });
                        } else {
                            context.succeed();  // within evaluation duration
                        }
                    } else {
                        context.succeed();  // not enough readings
                    }
                });
            }
        }
 
 
 

    });

    function crossedHighThreshold(event) {
        return event[MEASUREMENT] > MEASUREMENT_HIGH_THRESHOLD;
    }

    function crossedLowThreshold(event) {
        return event[MEASUREMENT] < MEASUREMENT_LOW_THRESHOLD;
    }

    function droppedBackWithinTolerance(event, alarmState) {
        if(alarmState.highThreshold) {
            return event[MEASUREMENT]  <= MEASUREMENT_HIGH_THRESHOLD;
        } else {
            return event[MEASUREMENT] >= MEASUREMENT_LOW_THRESHOLD;
        }
    }

    function failWithMessage(message) {
        console.log(message);
        context.fail(message);
    }

    function readCurrentAlarmState(callback) {
        var params = {
            TableName: ALARM_TABLE_NAME,
            Key: {
                sensor_id: MEASUREMENT_SENSOR_ID
            },
            ConsistentRead: true
        };
        docClient.get(params, function (err, data) {
            if(err) {
                callback(err);
            } else {
                var alarmState;
                if( ! data.Item ||  ! data.Item.alarmState) {
                    alarmState = { processing : false };
                } else {
                    alarmState = data.Item.alarmState;
                }
                callback(null, alarmState);
            }
        });
    }

    function updateAlarmProcessing(alarmState, callback) {
        var params = {
            TableName: ALARM_TABLE_NAME,
            Item: {
                sensor_id: MEASUREMENT_SENSOR_ID
            }
        };
        params.Item.alarmState = alarmState;
        docClient.put(params, callback);
    }

    function initializeAlarmProcessing(highThreshold, callback) {
        var alarmState = { processing : true, highThreshold : highThreshold, firstReadingTime : new Date().getTime(), readingCount : 1 };
        console.log('Initializing alarm processing state  ' + JSON.stringify(alarmState));
        updateAlarmProcessing(alarmState, callback);
    }

    function clearAlarmProcessing(callback) {
        var alarmState = { processing : false };
        console.log('clearing alarm processing ');
        updateAlarmProcessing(alarmState, callback);
    }

    function fireAlarm(alarmState, callback) {
        var alarmActions = alarmState.highThreshold ? HIGH_THRESHOLD_ALARM_ACTIONS : LOW_THRESHOLD_ALARM_ACTIONS;
        console.log('Firing alarm with state ' + JSON.stringify(alarmState, null, 2) + ' mqtt messages ' + JSON.stringify(alarmActions, null, 2));
        var publishPending = alarmActions.length;
        alarmActions.map( function(alarmAction) {
            icebreakerMqttClient.sendCommandToActuator(alarmAction.actuator, alarmAction.message, function(err) {
                if(--publishPending == 0) {
                    callback(err);
                }
            });
        });
    }

};

Credits

Atul

Atul

1 project • 22 followers
Thanks to Jordin Green and Dave Kush.

Comments