Author Topic: SMS Gateway Interface - send SMS commands to cause events  (Read 1056 times)

HeneryH

  • Full Member
  • ***
  • Posts: 229
SMS Gateway Interface - send SMS commands to cause events
« on: November 21, 2018, 03:59:07 PM »
I got started down this path as one possible solution to my system needs.  I think I may choose a built-in solution for my specific need rather than the SMS solution but this feature is pretty cool for other user scenarios.

  • The original task that I'm not actually going to use this for is - If sensor x shows a particular reading then turn on device y.  I thought the only way to get this done was using a third party IFTTT service and I would have needed an SMS input.  Turns out there is a built-in gateway feature called polling events that is better suited to this use case.  Perhaps. 
  • The next use-case I am going to use is a way for users to send an SMS message to open a gate.  I have clients that access the property through a locked gate.  The lock is a 12v magnetic lock.  The current standalone keypad I have is now getting keys stuck and hard to use.  The proposed solution is to replace the keypad with a simple SwitchMote for the 12v gate magnet.  When a user (specified list of authorized sending phone numbers) sends a command to my Twilio phone number the gate will unlock for a specified period of time then relock.  I can customize the accepted SMS commands to a limited set that I define.  The full gateway web interface is just too much and too powerful for my clients.  I just want a simple way for them to open the gate that has basic security.
  • There might be other use cases for SMS commands.
  • spark's use-case was sending a text to open/close the garage and secondarily send an sms alert if it stays open for a period of time.

Credits to Brad/spark for an old post he did that got me started on this.  I never got his code to work but the snippets he wrote sent me down the path that got me to a working solution.

I'll post the method here in a few instructory posts along with the final complete sms_gateway.js app that works.

First a couple of tutorial posts...
« Last Edit: November 21, 2018, 07:47:31 PM by HeneryH »

HeneryH

  • Full Member
  • ***
  • Posts: 229
Re: SMS Gateway Interface - send SMS commands to cause events
« Reply #1 on: November 21, 2018, 04:06:19 PM »
The basic architecture uses the Twilio service where you can sign up for a free phone number that is capable of initiating web-hooks based on incoming text messages.

https://www.twilio.com/docs/usage/tutorials/how-to-use-your-free-trial-account

Sign up for a free account and one free number.  Any texts from the free account will have a pre-amble on them that can be removed with a paid plan.

https://www.twilio.com/docs/usage/tutorials/how-to-use-your-free-trial-account

There are some great starter tutorials.

Here is a basic tutorial for a node.js application that fires up a node express web app that listens for POST connections to your <domain>/sms url.

Put a port forward or reverse proxy config on your home router that forwards connections requests to the http://<domain>/sms url to your RaspberryPi.

Run this node.js app on your Pi. 

It will respond to any inbound SMS with a reply text.

https://www.twilio.com/docs/sms/tutorials/how-to-receive-and-reply-node-js
Code: [Select]
const http = require('http');
const express = require('express');
const MessagingResponse = require('twilio').twiml.MessagingResponse;

const app = express();

app.post('/sms', (req, res) => {
  const twiml = new MessagingResponse();

  twiml.message('The Robots are coming! Head for the hills!');

  res.writeHead(200, {'Content-Type': 'text/xml'});
  res.end(twiml.toString());
});

http.createServer(app).listen(1337, () => {
  console.log('Express server listening on port 1337');
});

Here is the package.json to help load things up:
Code: [Select]
{
  "//1": "describes your app and its dependencies",
  "//2": "https://docs.npmjs.com/files/package.json",
  "//3": "updating this file will download and update your packages",
  "name": "sms_gateway",
  "version": "0.0.1",
  "description": "A simple Node app built on Express, instantly up and running.",
  "main": "sms_gateway.js",
  "scripts": {
    "start": "node sms_gateway.js"
  },
  "dependencies": {
    "console-stamp": "^0.2.7",
    "express": "^4.16.4",
    "lodash": "^4.17.11",
    "socket.io-client": "^2.1.1",
    "twilio": "^3.24.0"
  },
  "engines": {
    "node": "8.x"
  },
  "repository": {

  },
  "license": "MIT",
  "keywords": [
    "node",
    "glitch",
    "express"
  ]
}

This is a good start for replying to incoming messages.  The library replies on the same socket as the incoming request so it can't be used for unsolicited outbound message.

That is coming...
« Last Edit: November 21, 2018, 06:58:38 PM by HeneryH »

HeneryH

  • Full Member
  • ***
  • Posts: 229
Re: SMS Gateway Interface - send SMS commands to cause events
« Reply #2 on: November 21, 2018, 04:16:26 PM »
This is a good start.  but how do I start interacting with the gateway app?

I did some reading on the architecture Felix put together and tried to reverse engineer the system.

There is a gateway.js app that runs on the Pi.  This gateway app talks to motes via serial port and talks to client connections through a socket.io connection.

I took a look at the index.html file to understand the way the clients talk to the gateway over socket.io.

Actually with just some relatively simple editing (remove all of the visual aspects of the dashboard elements), I took the javascript directly from the client index.html file and used that as a base for my sms_gateway.js app.

When any client connects to the gateway, the gateway replies with a bunch of data about all of the nodes currently running on the gateway.

My first cut and getting the two to talk was, a basic test.  On an incoming SMS, make a connection to the gateway and spit back a string-ified version of the node database along with all of their metrics.

This was a good second step.  It shows incoming SMS messages are received and the sms_gateway can connect to the gateway and spit back results.

The code I lifted was basically pulling all of the socket.io event handlers from the index.html file and putting them into my sms_gateway.

Code: [Select]
var socketURL = 'http://192.168.1.234:8080';                              // assumes this runs on the gateway Pi
var socket = require('socket.io-client')(socketURL);                      // Socket to the Pi Gateway

/*
 * SOCKET EVENT HANDLERS, incoming socket messages from the PiGateway follow the following protocols:
 * [list out the messages here...
 */
socket.on('connect', function () {
    console.log('Socket connected!')
});

socket.on('connect_error', function (err) {
    console.warn('Error connecting: ', err)
});

socket.on('connect_timeout', function () {
    console.warn('Timeout trying to connect')
});

// These happen right after connections
socket.on('MOTESDEF', function(motesDefinition) {
  motesDef = motesDefinition;
//  $("#nodeMoteType").empty();
//  $('#nodeMoteType').append('<option value="">Select type...</option>');
//  for(var mote in motesDef)
//    $('#nodeMoteType').append('<option value="' + mote + '">' + motesDef[mote].label || mote + '</option>');
//  $("#nodeMoteType").selectmenu();
});

socket.on('METRICSDEF', function(metricsDefinition) {
  metricsDef = metricsDefinition;
});

socket.on('EVENTSDEF', function(eventsDefinition) {
  eventsDef = eventsDefinition;
//  $('#addEventType').change(function() {
//    if ($(this).val())
//    {
//      $('#addEventDescr').html('<span style="color:#000">Action: </span>' + (eventsDef[$(this).val()].icon ? '<span class="ui-btn-icon-notext ui-icon-'+eventsDef[$(this).val()].icon+'" style="position:relative;float:left"></span>' : '') + (eventsDef[$(this).val()].descr || key));
//      $('#addEvent_OK').show();
//    }
//    else {
//      $('#addEventDescr').html(' ');
//      $('#addEvent_OK').hide();
//    }
//  });
});

socket.on('SETTINGSDEF', function(newSettingsDef) {
  settingsDef = newSettingsDef;
//  $('#settingsList').empty();
//  settingsDefBindMap = {};
//  for(var sectionName in settingsDef)
//  {
//    var sectionSettings = settingsDef[sectionName];
//    if (!sectionSettings.exposed) continue;
//    var sectionLI = $('<li data-role="list-divider">'+sectionName+'</li>');
//    $('#settingsList').append(sectionLI);
//   
//    for(var settingName in sectionSettings)
//    {
//      var setting = sectionSettings[settingName];
//      if (setting.exposed === false) continue;
//      if (setting.value == undefined) continue;
//      var settingLI;
//      var type = 'type="'+(setting.type || 'text')+'"';
//      if (setting.type == 'range') type+= ' min="'+setting.min+'" max="'+setting.max+'" step="0.01"';
//      if (setting.type == 'checkbox') type+= (setting.value == 'true' ? ' checked' : ' unchecked') + ' ';
//      if (setting.description)
//      {
//        settingLI = $('<li class="ui-field-contain">'
//          +'<label>'
//          +'<a href="#popupInfo-'+sectionName+'-'+settingName+'" data-rel="popup" data-transition="pop">'+settingName+'</a>'
//          +'<div id="popupInfo-'+sectionName+'-'+settingName+'" data-role="popup" data-overlay-theme="b" class="popupInfo">'
//          +'<b>'+settingName+'</b>: '+setting.description
//          +'</div>'
//          +':</label>'
//          + (setting.type=='checkbox' ? '<label for="'+sectionName+'-'+settingName+'">'+setting.value+'</label>' : '')
//          +'<input '+type+' name="'+sectionName+'-'+settingName+'" id="'+sectionName+'-'+settingName+'" value="'+setting.value+'"'+(setting.editable===false?' data-clear-btn="false" readonly="readonly"':' data-clear-btn="true"')+'></li>');
//      }
//      else
//      {
//        settingLI = $('<li class="ui-field-contain">'
//          +'<label>'+settingName+':</label>'
//          + (setting.type=='checkbox' ? '<label for="'+sectionName+'-'+settingName+'">'+setting.value+'</label>' : '')
//          +'<input '+type+' name="'+sectionName+'-'+settingName+'" id="'+sectionName+'-'+settingName+'" value="'+setting.value+'"'+(setting.editable===false?' data-clear-btn="false" readonly="readonly"':' data-clear-btn="false"')+'></li>');
//      }
//      settingsDefBindMap[sectionName+'.'+settingName+'.value'] = '#'+sectionName+'-'+settingName;
//      $('#settingsList').append(settingLI);
//     
//      //for range INPUTs, we need to rehidrate the value in the INPUT.value because jqueryUI does not do it (it injects an anchor where it keeps the value in an attribute, doh!)
//      if (setting.type=='range' || setting.type == 'checkbox')
//      {
//        $(document).on("change", '#'+sectionName+'-'+settingName, { section:sectionName, setting: settingName }, function(e) {
//          newVal = (this.type=='checkbox' ? this.checked.toString() : $(this).val());
//          $('label[for*='+e.data.section+'-'+e.data.setting+']').html(newVal);
//          boundSettings[e.data.section][e.data.setting].value = newVal;
//        });
//      }
//    }
//  }
//  $('#settingsList').listview().listview('refresh').trigger("create");
//  boundSettings = Bind(settingsDef, settingsDefBindMap);
});

socket.on('UPDATENODE', function(entry) {
  updateNode(entry);
  console.log('Received update:');

//  refreshNodeListUI();
});

//*
//* This is a helper function for the socket callback that occurs when in inbound node update message is received from the gateway.
//*
function updateNode(node) {
  console.log(JSON.stringify(node));

  // for now, just spit it out to the Twilio sms phone number.
  twilio.messages
    .create({
       body: JSON.stringify(node),
       from: twilioNumber,
       to: authorizedSubscribers[0]
     })
    .then(message => console.log("Sending to SMS: ", message.sid, "  Status: ", message.status, "\n", JSON.stringify(node)))
    .catch(e => { console.error('Got an error:', e.code, e.message); })
    .done();
 
 // everything below was primarily updating the web portal UI which I have mostly just commented out.
  if (isNumeric(node._id))
  {
    nodes[node._id] = node;
    var nodeValue = metricsValues(node.metrics);
    var lowVoltageSetting = node.settings && node.settings['lowVoltageValue'] ? node.settings['lowVoltageValue'] : (motesDef[node.type] && motesDef[node.type].settings && motesDef[node.type].settings.lowVoltageValue ? motesDef[node.type].settings.lowVoltageValue : 0 /*boundSettings.general.lowVoltageValue.value*/);
    var lowBat = node.metrics.V != null && node.metrics.V.value < lowVoltageSetting;
//    var newLI = $('<li id="' +  node._id + '"><a node-id="' + node._id + '" href="#nodedetails" class="nodedetails"><img class="productimg" src="images/'+getNodeIcon(node)+'"><h2>' + (nodeResolveString(node.label, node.metrics) || node._id) + ' ' + resolveRSSIImage(node) + ' ' + (lowBat ? '<img src="images/lowbattery.png" class="lowbattimg"/> ' : '') + ago(node.updated, 0).tag + (node.hidden ? ' <img class="listIcon20px" src="images/icon_hidden.png" />' : '') + '</h2><p>' + (nodeResolveString(node.descr || "", node.metrics) || '&nbsp;') + '</p>' + (nodeValue ? '<span class="ui-li-count ui-li-count16">' + nodeValue + '</span>' : '') + '</a></li>');
//    var existingNode = $('#nodeList li#' + node._id);
//    if (node.hidden)
//      if (showHiddenNodes)
//        newLI.addClass('hiddenNodeShow');
//      else
//        newLI.addClass('hiddenNode');
//    if(existingNode.length)
//      existingNode.replaceWith(newLI);
//    else $('#nodeList').append(newLI);
//    if (node._id == selectedNodeId) refreshNodeDetails(node);
//    //update this node's events on the events page
//    for(var key in node.events)
//    {
//      var evt = eventsDef[key];
//      if (!evt) continue;
//
//      var enabled = node.events[key].enabled;
//      var execTime = node.events[key].executeDateTime;
//      var dt = new Date(execTime);
//      var execTimeString = execTime ? (' Due in <b>' + ago(dt.getTime(), 0).tag + '</b>') : '';
//      var remainingExecTimeString = (dt.getDate() == new Date().getDate())
//                                    ? execTime ? ' @ <b>' + dt.format('h:MMt') + '</b>' : ''
//                                    : execTime ? ' on <b>' + dt.format('mmm-d @ h:MMt') + '</b>' : '';
//      var newLI = $('<li id="'+node._id+'-'+key+'"><a data-icon="delete" node-id="' + node._id + '" event-id="' + key + '" href="#" class="eventEnableDisable"><span class="ui-btn-icon-notext ui-icon-'+ (enabled ? (evt.icon ? evt.icon : 'action') : 'minus') + (enabled ? " enabled" : " disabled") + '"></span><h2>[' + node._id + '] ' + evt.label + '</h2><p>Action: ' + (evt.descr || '&nbsp;') + '</p><p>' + execTimeString + remainingExecTimeString + '</p></a><a node-id="' + node._id + '" event-id="' + key + '" href="#" class="eventDelete" data-transition="pop" data-icon="delete"></a></li>');
//
//      var allEventsListNode = $('#allEventsList li#' + node._id + '-' + key);
//      if(allEventsListNode.length)
//        allEventsListNode.replaceWith(newLI);
//      else $('#allEventsList').append(newLI);
    }
//    $('#allEventsList').listview().listview('refresh');
  }


« Last Edit: November 21, 2018, 08:12:34 PM by HeneryH »

HeneryH

  • Full Member
  • ***
  • Posts: 229
Re: SMS Gateway Interface - send SMS commands to cause events
« Reply #3 on: November 21, 2018, 04:25:46 PM »
So far I have the sms_gateway.js app being able to connect to the gateway.js app and pull all of the current topology of motes.

Next I want to send commands to various motes when a particular SMS message is received.

This was done by emulating what the web service does when a user clicks a web button.  The clients jam a CONTROLCLICK message down the socket with a message fitting a particular pattern.

Here is the code for how to send a command.
Code: [Select]
Copy the code here.
Sorry, it is easier at the moment for me to compose here on one computer and paste the code from another...