prey/prey-node-client

View on GitHub
lib/agent/triggers/control-zones/index.js

Summary

Maintainability
C
7 hrs
Test Coverage
"use strict";

//////////////////////////////////////////
// Prey Geofencing Plugin
// (c) 2012 - Fork Ltd.
// by Tomas Pollak and Javier Acuña - http://forkhq.com
// GPLv3 Licensed
//
// The geofencing module takes care of notifying the Prey servers
// when the device either gets in or out of a specific geofence.
//
// The command signature is as follows:
//
//  command: 'watch',
//  target: 'geofencing',
//  options: {
//    locations: [{
//      id: 'someId',
//      lat: '-33.4230905',
//      lng: '-70.6138094',
//      radius: 100,
//      direction: 'in'|'out'|'both',
//      expires: -1
//    }]
//  }
//
//////////////////////////////////////////

var join      = require('path').join,
    base_path = join(__dirname, '..', '..'),
    hooks     = require(join(base_path, 'hooks')),
    logger    = require('./../../common').logger.prefix('geofencing'),
    LatLon    = require('./../location/lib/latlng'),
    action    = require('./../../actions/geofencing'),
    storage   = require('./../../utils/storage'),
    api       = require('./../../control-panel/api'),
    Emitter   = require('events').EventEmitter;

var emitter,
    zones,
    location,
    attempts = 0,
    retryTimeOut,
    checking = false;

let timerActionStart;

var push_event = function(type, zone_id, coords) {
  coords.id = zone_id.id;
  var data = {
    name: type,
    info: coords
  }
  var opts = { json: true };
  api.push.methods['event'](data, opts);
}

var getDistance = function(latlng1, latlng2) {
  var p1 = new LatLon(latlng1.lat, latlng1.lng);
  var p2 = new LatLon(latlng2.lat, latlng2.lng);
  return p1.distanceTo(p2);
}

var fibonacci = function(attempt) {
  if (attempt == 0) return 0;
  else if (attempt == 1) return 1;
  else return (fibonacci(attempt - 1) + fibonacci(attempt - 2));
}

var check_location = function(delay) {
  attempts++;
  retryTimeOut = setTimeout(function() {
    hooks.trigger('get_location', 'geofencing');
  }, delay * 1000 * 60) // Delay in minutes: 0, 1, 1, 2, 3, 5, 8, 13, ....
}

var check_zones = function() {
  if (!location || checking) return;
  checking = true;

  zones.forEach(function (zone) {
    var origin = {lat: zone.lat, lng: zone.lng};

    logger.debug('Verifying geofence: ' + zone.id + ', Radius: ' + zone.radius);
    logger.debug("Current location: " + location.lat + "," + location.lng);

    // in case an origin wasnt passed, set the first location as the origin
    //if (!this.origin) return this.origin = { coords;
    var distance = getDistance(origin, location);

    logger.debug("Current distance from origin: " + distance);
    // distance comes in KM, so we transform to M to compare

    if (distance * 1000 > zone.radius) { // outside
      storage.do('update', { type: 'geofences', id: zone.id, columns: 'state', values: 'outside' }, (err) => {
          if (err) {
            logger.error(err);
            return;
          }
          if (zone.state == 'inside' && zone.direction !== 'in') {
            logger.info('Device left the geofence ' + zone.name + '! Notifying')
            hooks.trigger('geofencing_out', zone.id)
            push_event('geofencing_out', {id: zone.id}, location);
          }
          checking = false;
          zone.state = 'outside';
        });
    } else { // inside
      storage.do('update', { type: 'geofences', id: zone.id, columns: 'state', values: 'inside' }, (err) => {
        if (err) {
          logger.error(err);
          return;
        }
        if ((zone.state == 'outside' && zone.direction !== 'out') || zone.state == null) {
          logger.info('Device got inside the geofence ' + zone.name + '! Notifying')
          hooks.trigger('geofencing_in', zone.id)
          push_event('geofencing_in', {id: zone.id}, location);
        }
        checking = false;
        zone.state = 'inside';
      });
    }
  });
}

exports.start = function(opts, cb) {
  hooks.on('geofencing_start', function(fences) {
    if (!fences) return;

    zones = fences;

    if (!location) return;

    if (zones.length == 0) {
      attempts = 0;
      clearTimeout(retryTimeOut)
      return;
    }

    check_zones();
  });

  hooks.on('new_location', function(coords) {
    // Only check zones on significant location changes
    if (!coords || (coords.delta && coords.delta < 30)) return;

    // When a new trustful location comes it has to be saved, even if there are no fences
    if (coords.method == 'wifi' && coords.accuracy < 300)
      location = coords;

    if (!zones || zones.length == 0) return;

    // If the location isn't trustworthy ask the location again
    if ((coords.method != 'wifi' && coords.method != 'native') || coords.accuracy > 300)
      return check_location(fibonacci(attempts));

    clearTimeout(retryTimeOut)
    attempts = 0;

    check_zones();
  })

  // Fetch geofences and stop previous location check
  hooks.on('connected', function() {
    clearTimeout(retryTimeOut)
    attempts = 0;
    if (timerActionStart) clearTimeout(timerActionStart);
    timerActionStart = setTimeout(() => {
      action.start();
    }, 1000 * 15);
  })

  // No need to keep checking the location until it's connected again
  hooks.on('disconnected', function() {
    if (timerActionStart) clearTimeout(timerActionStart);
    clearTimeout(retryTimeOut)
    attempts = 0;
  })

  emitter = new Emitter();
  cb(null, emitter);

};

exports.stop = function() {
  hooks.remove('geofencing_start');
  hooks.remove('new_location');
  if (emitter) {
    emitter.removeAllListeners();
    emitter = null;
  }
};