App Note: Advanced device timeout handling and custom status widgets

Follow

Note: This article contains advanced usage of both the One Platform scripting and Portals widgets.

Introduction
A very typical need of almost every device used on Exosite is alerting if it is online or not and being able to check if your devices are online. The specific items required usually involve the following:
Determine a timeout condition (the device is off-line)
Alert something or someone of timeout conditions / Take action
Show device online status

What Portals Does
Portals tries to give you an indication of this for you. For example, on portals.exosite.com/manage/devices page, you should see a list of devices, a green or red dot indicating if it is online or not, and a 'Last Reported Time' category. Here is an example image of that:

 

Portals calculates 'Active' by looking at the 'Active Time' field that is stored in the device's meta information. This 'Active Time' is not a specific item of One Platform clients, but Portals adds it into the Device client's meta info, as an application detail. That field is by default 5 minutes if you add a device through Portals and you can edit it if you click on the device here and open it's 'pop up' window. ( More information about how Portals stores application specific info can be found here ).

Portals also calculates this 'Last Reported Time' based on the last value of any data source, no matter if the device actually wrote it or a script or a user wrote to a data source using a widget, so it can be misleading.

Creating a Timeout Events and Alerts in Portals
Portals also let's me create simple Events and Alerts, one of which is condition type 'timeout'. Here you can see in the graphic below how a simple wizard interface walks me through creating an event, which in the One Platform is a 'data rule'. This event is false until it detects that the selected data source hasn't reported data in the duration specified, in this case 60 seconds, at which point the event is true. I can then attach an 'alert' to this event on the same page. An alert is a 'dispatch' in the One Platform.

Improved Timeout / Status indication
So, how can this be improved for my application?  There are two parts to this document, one on creating a more advanced timeout detection and handling feature. The other is a more customized dashboard widget that is specific to your application needs. We'll detail both here below.

Create a Timeout / Status Script
A One Platform script has access to all information and resources of a client (device). This means that you can write your script to tailer the timeout condition and status reporting. Let's say your device is reporting multiple pieces of information, but only one data source really matters and tells you if it is online or not. We'll call this data source 'packet'. Let's also say you want to have alerts and widgets be able to communicate information like the last time you received the packet, how long it has been timed out if it has timed out, etc.
Here are the data sources I want to examine and/or write to with information.

  • packet - This is the actual data coming in from the device, it's the only thing that I care about determining if it is online or not
  • status - This data source will be used for alert messages and for a widget to tell me if the device is online, timed-out, how long it has timed out, etc
  • lastpacket - This data source will store the last reported timestamp of packet. (Yes I could get this from the packet data source but it handy for widgets and alerts to have as a data source.

My script will do the following things:

  1. Create these data sources if they don't exist already
  2. Set default values of status and lastpacket.
  3. Get meta information about the device that Portals specified (active time period, timezone, etc)
  4. Check every 60 seconds for new data into packet. 
    1. During this loop, get the last reported time of packet and compare against the current time.
    2. If the difference between current time and last reported timestamp is greater than the Portals specified 'Active Time' field, update the status data source with the text 'Timeout' and also specify how long it has been in this timeout condition.
    3. Write the last reported timestamp to lastpacket in human readable format.
    4. Send an email alert out if the condition (online again or timeout) is new with information about the state.


Here is my script:


-- Advanced Timeout Alert and Status
-- Exosite One Platform Script
-- 
-- Script check if data received on packet to 
-- determine activity / status of the device.
-- 
local device = alias[''] -- Represents this client device
local meta = json.decode(alias[''].meta) -- decode meta from device client
local dev_tz = meta.timezone -- device timezone as specified by Portals field
local dev_location = meta.location -- device location as specified by Portals field
local dev_acttime = meta.activetime -- device active time as specified by Portals field
-- User / Application preference settings
setlocale("en_US.utf8")
settimezone("UTC") 
local timeformat = '%b %d,%Y %T %Z'
debug('starting')
debug('Name: '.. device.name..',Active Timeout: '..dev_acttime..'m')
--
-- This is a list of data sources used by this script and 
-- this table is used to check and create if they don't exist
--
local dstable = {
 {alias="packet",name="Packet",format="string"},
 {alias="status",name="Status",format="string"},
 {alias="lastpacket",name="Last Packet Time",format="string"},
}
--
-- This is a routine that will check if data sources exist and
-- if not create
--
for i, ds in ipairs(dstable) do
 if not manage.lookup("aliased" ,ds.alias) then
 local description = {
 format = ds.format
 ,name = ds.name
 ,retention = {count = 1 ,duration = "infinity"}
 ,visibility = "private"
 }
 debug("creating datasource: "..ds.name.."(alias: "..ds.alias..")")
 local success ,rid = manage.create("dataport" ,description)
 if not success then
 debug("error initializing rid dataport: "..rid or "")
 return
 end
 manage.map("alias" ,rid ,ds.alias)
 end
end

-- Function to send alert
local function sendalert(state,detectedtime,lasttime)
 local emailaddress = 'emailaddresshere'
 local emailsubject = ''
 local emailmessage = ''
 debug('sending alert')
 emailsubject = 'Alert: State Change: Device: '..device.name
 emailmessage = 'Detected State Change at %s\r\nDevice Name: %s\r\nLocation: %s\r\nNew State: %s\r\nLast Reported Time: %s\r\n'
 emailmessage = string.format(emailmessage,detectedtime,device.name,dev_location,state,lasttime)
 -- Send Alert Message
 local status,reason = email(emailaddress,emailsubject,emailmessage)
 if status == false then debug('Email failed, reason: '.. reason) end
end
-- Get variables for specific data sources to check/write
local packet = alias['packet']
local status = alias['status']
local packettime = alias['lastpacket']
local state = 'na'

-- Let user know that the script is checking status now
status.value = 'Checking..'
debug('starting routine loop')
while true do
 local ts = packet.wait(now+60) -- run this loop every 60 seconds no matter if new data or not
 meta = json.decode(alias[''].meta) -- update meta data, which is in json format
 dev_acttime = meta.activetime -- update active time
 local p_ts = packet.timestamp
 if p_ts ~= nil then
 if (now - p_ts) > dev_acttime*60 then
 if state ~= 'timeout' then
 status.value = 'Timeout (>'..dev_acttime..' m)'
 packettime.value = string.format('%s',date(timeformat,p_ts))
 debug('timeout (>'..dev_acttime..' m) '..string.format('%s',date(timeformat,p_ts)))
 state = 'timeout'
 sendalert(state,date(timeformat,now),date(timeformat,p_ts))
 end
 else
 if state ~= 'running' then
 state = 'running'
 status.value = 'Running'
 debug('running - '..string.format('%s',date(timeformat,p_ts)))
 sendalert(state,date(timeformat,now),date(timeformat,p_ts))
 end
 packettime.value = string.format('%s',date(timeformat,p_ts))
 end
 else --no data at all, no timestamp
 if state ~= 'timeout' then
 status.value = "Has not reported"
 packettime.value = string.format('never')
 debug('timeout (>'..dev_acttime..' m) '..string.format("hasn't reported yet"))
 state = 'timeout'
 sendalert(state,date(timeformat,now),'never')
 end
 end
end

When this runs, I get an email like this with a state change (timeout detected or data written to packet after timeout).

Create a Custom Status Widget
On to the device list widget. We saw what Portals offers on the /manage/device page. There is a 'Device List' widget that is off the shelf and as of writing this document looks something like this, where 'Active' shows as Green or Red depending on last written value of any data source. Another off the shelf widget is the 'Device Data Table' widget. These are shown below, one the Device Data table widget I'm showing the two data sources I created in my script (status and lastpacket).

Portals Off-the-Shelf Widgets

Let's build a widget that is specific to my application need.  First, let's add a custom widget to my dashboard.

I want to have columns for the device name, status, last packet, and some data sources. Custom widgets are written in Javascript and in this example, I'm going to take advantage of the Google Chart APIs.


Here is what this custom widget looks like.

Here is the custom widget javascript code

 
/**
 * Table
 * @version 1.0.0
 */
// select one or more data sources from the list above to view this demo.
function(container,portal)
{
  //console.log("starting sensor table widget")
  function errorMsg( message )
  {
    var h3 = document.createElement('h3'),
        strong = document.createElement('strong'),
        p = document.createElement('p')
    ;
    container.appendChild( h3 );
    h3.appendChild( strong );
    strong.innerHTML = 'ERROR:';
    container.appendChild( p );
    p.innerHTML = message;
    container.style.margin = '20px';
    container.style.color = '#a60000';
  }
  function addCSSclass(className, classRule) 
  {
    if (document.all) {
      document.styleSheets[0].addRule("." + className, classRule);
    } 
    else if (document.getElementById) {
      document.styleSheets[0].insertRule("." + className + " { " + classRule + " }", 0);
    }
  }

  function googleChart( portal )
  {
    var i,
        d,
        group,
        output = { cols: [], rows: [] },
        devicemeta,
        deviceclient,
        ds,
        packet,
        lastpacket,
        status,
        statusFormat,
        statusVal,//0 unknown, 1 running, 2 timeout
        rowFormat//default
    ;

    console.log("start through device list");
    for( d = 0; d < portal.clients.length; d++ )
    {
      //default values for each client
      lastpacket = "n/a";
      packet = "n/a"
      status = "n/a";
      statusFormat = '<img src="' + '/static/png/indicator_grey_lamp.png' 
        + '"'+'style="vertical-align:middle"' 
        +'/> '+'not available';
      statusVal = 0;//0 unknown, 1 running, 2 timeout
      rowFormat = 'line-height:1em'; //default
      group = [];
      deviceclient = portal.clients[d];
      devicemeta = JSON.parse( deviceclient.info.description.meta);
      for (i = 0; i < deviceclient.dataports.length; i++)
      {
        //console.log(i)
        //console.log(deviceclient.dataports[i].alias)
        ds = deviceclient.dataports[i];
        if (ds.alias == 'lastpacket')
        {
          try{
            lastpacket = ds.data[0][1];
          }
          catch(err){
            lastpacket = "error";
          }
        }
        if (ds.alias == 'packet')
        {
          try{
            packet = ds.data[0][1];
          }
          catch(err){
            packet = "error";
          }
        }        if (ds.alias == 'status')
        {
          try
          {
            status = ds.data[0][1];
            if(status.search("Running")==-1)
            {
              //statusFormat = '<img src="' + '/static/png/not_available.png' + '"/>'
              statusFormat = '<img src="' + '/static/png/indicator_red_lamp.png' + '"'+'style="vertical-align:middle"' +'/> '+status;
              statusVal = 2;
              //rowFormat = 'background-color: #f8cccc; font-weight:bold; line-height:1em';
              //rowFormat = 'font-weight:bold; line-height:1em';
            }
            else
            {
              //statusFormat = '<img src="' + '/static/png/available.png' + '"/>'
              statusFormat = '<img src="' + '/static/png/indicator_green_lamp.png' + '"'+'style="vertical-align:middle"' +'/> '+status;
              statusVal = 1;
            }
          }
          catch(err)
          {
            statusFormat = '<img src="' + '/static/png/indicator_grey_lamp.png' + '"'+'style="vertical-align:middle"' +'/> '+'error';
            statusVal = 0;
          }
        }
      }
      
      //console.log("Create Row");
      group[0] = { v: deviceclient.info.description.name, p:{style:rowFormat} };
      group[1] = { v: statusVal, f: statusFormat, p:{style:rowFormat}};
      group[2] = { v: lastpacket, p:{style:rowFormat}};
      group[3] = { v: packet, p:{style:rowFormat}}
      group[4] = { v: 'interesting data', p:{style:rowFormat}};
      group[5] = { v: '1024', p:{style:rowFormat}};
      output.rows.push( { c: group } );
    }
    output.cols.push( { label: 'Device', type: 'string' } );
    output.cols.push( { label: 'Status', type: 'string' } );
    output.cols.push( { label: 'Last Packet', type: 'string' } );
    output.cols.push( { label: 'Packet Value', type: 'string' } );
    output.cols.push( { label: 'Other Data', type: 'string' } );
    output.cols.push( { label: 'More Data', type: 'number' } );
    addCSSclass("bold-font", "font-weight: bold;");
    // draw google chart
    google.load( 'visualization', '1',
    {
      packages: ['table'],
      callback: drawChart
    } );
    function drawChart()
    {
      var cssCustom = {
          'headerRow': '',
          'tableRow': '',
          'oddTableRow': '',
          'selectedTableRow': '',
          'hoverTableRow': '',
          'headerCell': '',
          'tableCell': '',
          'rowNumberCell': ''},
          data = new google.visualization.DataTable( output ),
          view = new google.visualization.DataView(data),
          table = new google.visualization.Table( container ),
          options =
          {
            width: '100%',
            //height: '100%',
            //showRowNumber: true,
            alternatingRowStyle:true,
            allowHtml:true,
            page: 'disable',
            pageSize: 5,
            pagingSymbols:
            {
              prev: 'prev',
              next: 'next'
            },
            pagingButtonsConfiguration: 'auto',
            sort:'enable',
            sortAscending:false,
            sortColumn:1,
            cssClassNames: cssCustom
          }
      ;
      //view.hideColumns([3]);
      table.draw( view, options );
      google.visualization.events.addListener(table, 'select', function() {
        var row
        ;
        row = table.getSelection()[0].row;
        table.setSelection();
        alert('You selected ' + data.getValue(row, 0) +'\r\nCould use this to go to another page or change other widgets');
      });
    }
  }
  // return if no data sources are selected.
  if( portal.clients.length < 1 )
  {
    errorMsg('No Devices Available');
    return;
  }
  // google chart
  googleChart( portal );
}
 

 

Here is a look at my dashboard showing each of these widgets (off the shelf vs my new custom widget) for a comparison. (Note I can change the size of the custom widget)


See how the 'My Devices' widget (device list widget) shows that both devices are active because a data source for both has been written to in respective active time period. For my application need, I only want to know if the data source 'packet' has been written to, which my custom widget tells me.

If I am domain admin (portals solution), I can actually then create a domain widget from this custom widget code that I can publish and use including auto populating device and data sources without having to select what is available in the custom widget editor.


That's it. Hopefully this is useful when thinking about your application needs.

 

  

Have more questions? Submit a request

Comments

Powered by Zendesk