You are here

farm_sensor_listener.module in farmOS 7

File

modules/farm/farm_sensor/farm_sensor_listener/farm_sensor_listener.module
View source
<?php

/**
 * @file
 * Code for the Farm Sensor Listener module.
 */
include_once 'farm_sensor_listener.features.inc';

/**
 * Implements hook_menu().
 */
function farm_sensor_listener_menu() {
  $items = array();

  // Callback for returning data.
  $items['farm/sensor/listener/%'] = array(
    'page callback' => 'farm_sensor_listener_page_callback',
    'page arguments' => array(
      3,
    ),
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implements hook_entity_delete().
 */
function farm_sensor_listener_entity_delete($entity, $type) {

  // Only act on farm asset entities.
  if ($type != 'farm_asset') {
    return;
  }

  // Only act on sensor assets.
  if ($entity->type != 'sensor') {
    return;
  }

  // Only act on "listener" type sensors.
  if ($entity->sensor_type != 'listener') {
    return;
  }

  // Delete data from the {farm_sensor_data} table.
  db_delete('farm_sensor_data')
    ->condition('id', $entity->id)
    ->execute();
}

/**
 * Implements hook_mail().
 */
function farm_sensor_listener_mail($key, &$message, $params) {

  // Assemble the notification email.
  if ($key == 'listener_notification') {

    // Build the email subject.
    $message['subject'] = t('Sensor notification from @sensor', array(
      '@sensor' => $params['sensor']->name,
    ));

    // Build the email body.
    $condition = '';
    switch ($params['condition']) {
      case '>':
        $condition = 'greater than';
        break;
      case '<':
        $condition = 'less than';
        break;
    }
    $message['body'][] = t('The latest sensor reading "@name" is !condition @threshold. Actual value: @value', array(
      '@name' => $params['name'],
      '!condition' => $condition,
      '@threshold' => $params['threshold'],
      '@value' => $params['value'],
    ));
    $uri = entity_uri('farm_asset', $params['sensor']);

    // We use htmlspecialchars() so that apostrophes are not escaped.
    $entity_label = htmlspecialchars(entity_label('farm_asset', $params['sensor']));
    $message['body'][] = $entity_label . ': ' . url($uri['path'], array(
      'absolute' => TRUE,
    ));
  }
}

/**
 * Callback function for processing GET and POST requests to a listener.
 * Handles receiving JSON over HTTP and storing data to the {farm_sensor_data}
 * table. Serves data back via API requests with optional parameters.
 *
 * @param $public_key
 *   The public key of the sensor that is pushing the data.
 *
 * The private key should be provided as a URL query string.
 *
 * Use HTTPS to encrypt data in transit.
 *
 * JSON should be in the following format:
 *   {
 *     "timestamp": 1234567890,
 *     "sensor1": 76.5,
 *     "sensor2": 55,
 *   }
 * Where:
 *   "timestamp" is an optional Unix timestamp of the sensor reading. If it's
 *   omitted, the time that the request is received will be used instead.
 *   The rest of the JSON properties will be considered sensor readings, and
 *   should be in the form "[name]": [value]. The value should be a decimal or
 *   integer, and will be stored as a fraction (numerator and denominator) in
 *   the database, for accurate precision.
 *
 * @return int
 *   Returns MENU_FOUND, MENU_ACCESS_DENIED, or MENU_NOT_FOUND.
 */
function farm_sensor_listener_page_callback($public_key) {

  // Load the private key from the URL query string.
  $params = drupal_get_query_parameters();

  // Look up the sensor by it's public key.
  $sensor = farm_sensor_listener_load($public_key);

  // If no asset was found, bail.
  if (empty($sensor)) {
    return MENU_NOT_FOUND;
  }

  // If this is a GET request, and the sensor allows public API read access,
  // proceed. Otherwise check the private key.
  if (!($_SERVER['REQUEST_METHOD'] == 'GET' && !empty($sensor->sensor_settings['public_api']))) {
    if (empty($params['private_key']) || $params['private_key'] != $sensor->sensor_settings['private_key']) {
      return MENU_ACCESS_DENIED;
    }
  }

  // If this is a POST request, process the data.
  if ($_SERVER['REQUEST_METHOD'] == 'POST') {

    // Pull the data from the request.
    $data = drupal_json_decode(file_get_contents("php://input"));

    // If data was posted, process it.
    if (!empty($data)) {
      farm_sensor_listener_process_data($sensor, $data);
    }

    // Return success.
    return MENU_FOUND;
  }
  elseif ($_SERVER['REQUEST_METHOD'] == 'GET') {

    // Add 'Access-Control-Allow-Origin' header to allow pulling this data into
    // other domains.

    /**
     * @todo
     * Move this to a more official place, or adopt the CORS module in farmOS.
     */
    drupal_add_http_header('Access-Control-Allow-Origin', '*');

    // If the path includes "/summary" after the public key, output value
    // summary info and return success.
    $args = func_get_args();
    if (isset($args[1]) && $args[1] == 'summary') {
      $data = farm_sensor_listener_values_info($sensor->id);
      drupal_json_output($data);
      return MENU_FOUND;
    }

    // If the 'name' parameter is set, filter by name.
    $name = '';
    if (isset($params['name'])) {
      $name = $params['name'];
    }

    // If the 'start' parameter is set, limit results to timestamps after it.
    $start = NULL;
    if (isset($params['start'])) {
      $start = $params['start'];
    }

    // If the 'end' parameter is set, limit to results before it.
    $end = NULL;
    if (isset($params['end'])) {
      $end = $params['end'];
    }

    // If the 'limit' parameter is set, limit the number of results.
    $limit = 1;
    if (isset($params['limit'])) {
      $limit = $params['limit'];

      // On second thought... only allow 100k max data points. Otherwise, it's
      // possible to exhaust PHP's memory, which is a potential DDoS vector.
      // If the requested limit is 0, set it to the max automatically.
      // If more than the max is specified, return a 422 error code.
      $max = 100000;
      if ($limit == 0) {
        $limit = $max;
      }
      elseif ($limit > $max) {
        drupal_add_http_header('Status', '422 Unprocessable Entity: exceeds max limit of ' . $max);
        return MENU_FOUND;
      }
    }

    // If the 'offset' parameter is set, offset the results.
    $offset = 0;
    if (isset($params['offset'])) {
      $offset = $params['offset'];
    }

    // Get the data from the sensor.
    $data = farm_sensor_listener_data($sensor->id, $name, $start, $end, $limit, $offset);

    // Return the latest readings as JSON.
    drupal_json_output($data);

    // Return success.
    return MENU_FOUND;
  }
  else {
    return MENU_FOUND;
  }
}

/**
 * Process data posted to a sensor.
 */
function farm_sensor_listener_process_data($sensor, $data) {

  // If the data is an array of multiple data points, iterate over each and
  // recursively process.
  if (is_array(reset($data))) {
    foreach ($data as $point) {
      farm_sensor_listener_process_data($sensor, $point);
    }
    return;
  }

  // Generate a timestamp from the request time. This will only be used if a
  // timestamp is not provided in the JSON data.
  $timestamp = REQUEST_TIME;

  // If a timestamp is provided, ensure that it is in UNIX timestamp format.
  if (!empty($data['timestamp'])) {

    // If the timestamp is numeric, we're good!
    if (is_numeric($data['timestamp'])) {
      $timestamp = $data['timestamp'];
    }
    else {
      $strtotime = strtotime($data['timestamp']);
      if (!empty($strtotime)) {
        $timestamp = $strtotime;
      }
    }
  }

  // Iterate over the JSON properties.
  foreach ($data as $key => $value) {

    // If the key is "timestamp", skip to the next property in the JSON.
    if ($key == 'timestamp') {
      continue;
    }

    // If the value is not numeric, skip it.
    if (!is_numeric($value)) {
      continue;
    }

    // Process notifications.
    farm_sensor_listener_process_notifications($sensor, $key, $value);

    // Create a row to store in the database;
    $row = array(
      'id' => $sensor->id,
      'timestamp' => $timestamp,
      'name' => $key,
    );

    // Convert the value to a fraction.
    $fraction = fraction_from_decimal($value);
    $row['value_numerator'] = $fraction
      ->getNumerator();
    $row['value_denominator'] = $fraction
      ->getDenominator();

    // Enter the reading into the {farm_sensor_data} table.
    drupal_write_record('farm_sensor_data', $row);

    // Invoke hook_farm_sensor_listener_data().
    module_invoke_all('farm_sensor_listener_data', $sensor, $key, $value);
  }
}

/**
 * Threshold condition check.
 *
 * @param $value
 *   The value being checked.
 * @param $condition
 *   The condition (< or >).
 * @param $threshold
 *   The threshold to compare against.
 *
 * @return bool
 *   Returns TRUE if the condition passes, FALSE otherwise.
 */
function farm_sensor_listener_check_condition($value, $condition, $threshold) {

  // Start with an assumption that the condition does not pass.
  $pass = FALSE;

  // Switch through available conditions.
  switch ($condition) {

    // Greater than threshold.
    case '>':
      if ((double) $value > (double) $threshold) {
        $pass = TRUE;
      }
      break;

    // Less than threshold.
    case '<':
      if ((double) $value < (double) $threshold) {
        $pass = TRUE;
      }
      break;
  }

  // Return the result.
  return $pass;
}

/**
 * Process sensor notifications.
 *
 * @param FarmAsset $sensor
 *   The sensor entity.
 * @param string $data_name
 *   The name assigned to the data.
 * @param $value
 *   The data value.
 */
function farm_sensor_listener_process_notifications($sensor, $data_name, $value) {

  // Send notifications, if the conditions are met.
  if (!empty($sensor->sensor_settings['notifications'][0]['name']) && !empty($sensor->sensor_settings['notifications'][0]['condition']) && !empty($sensor->sensor_settings['notifications'][0]['threshold']) && !empty($sensor->sensor_settings['notifications'][0]['email'])) {

    // Get all the variables.
    $name = $sensor->sensor_settings['notifications'][0]['name'];
    $condition = $sensor->sensor_settings['notifications'][0]['condition'];
    $threshold = $sensor->sensor_settings['notifications'][0]['threshold'];
    $email = $sensor->sensor_settings['notifications'][0]['email'];

    // Only proceed if the name matches.
    if ($name != $data_name) {
      return;
    }

    // Check to see if the condition was met.
    $pass = farm_sensor_listener_check_condition($value, $condition, $threshold);

    // If the condition didn't match, bail.
    if (!$pass) {
      return;
    }

    // Perform a check to see if the condition had already been met by the
    // previous data point that came in, and prevent the email from being sent
    // if that is the case.
    if ($pass) {

      // Load the last recorded data point.
      $query = db_select('farm_sensor_data', 'fsd');
      $query
        ->fields('fsd', array(
        'value_numerator',
        'value_denominator',
      ));
      $query
        ->condition('fsd.id', $sensor->id);
      $query
        ->condition('fsd.name', $name);
      $query
        ->orderBy('fsd.timestamp', 'DESC');
      $query
        ->range(0, 1);
      $row = $query
        ->execute()
        ->fetchAssoc();

      // If a numerator and denominator were returned...
      if (isset($row['value_numerator']) && isset($row['value_denominator'])) {

        // Calculate the decimal value and check if it passes the condition.
        $last_value = fraction($row['value_numerator'], $row['value_denominator'])
          ->toDecimal(0, TRUE);
        if (farm_sensor_listener_check_condition($last_value, $condition, $threshold)) {

          // If the condition passes, it means the last data point already
          // sent an alert, so don't send it again.
          $pass = FALSE;
        }
      }
    }

    // Send the email.
    if ($pass) {
      $params = array(
        'sensor' => $sensor,
        'name' => $name,
        'value' => $value,
        'condition' => $condition,
        'threshold' => $threshold,
        'email' => $email,
      );
      drupal_mail('farm_sensor_listener', 'listener_notification', $email, language_default(), $params);
      watchdog('farm_sensor_listener', 'Sensor id ' . $sensor->id . ' notification email sent. (' . $value . ' ' . $condition . ' ' . $threshold . ')');
    }
  }
}

/**
 * Fetch sensor data from the database.
 *
 * @param $id
 *   The sensor asset ID.
 * @param $name
 *   The sensor value name.
 * @param $start
 *   Filter data to timestamps greater than or equal to this start timestamp.
 * @param $end
 *   Filter data to timestamps less than or equal to this end timestamp.
 * @param $limit
 *   The number of results to return. Defaults to 1. If this is 0, no limit will
 *   be applied.
 * @param $offset
 *   The value to start at, used in combination with $limit.
 *
 * @return array
 *   Returns an array of data.
 */
function farm_sensor_listener_data($id, $name = '', $start = NULL, $end = NULL, $limit = 1, $offset = 0) {

  // Query the database for data from this sensor.
  $query = db_select('farm_sensor_data', 'fsd');
  $query
    ->fields('fsd', array(
    'timestamp',
    'name',
    'value_numerator',
    'value_denominator',
  ));
  $query
    ->condition('fsd.id', $id);

  // If a name is specified, filter by name.
  if (!empty($name)) {
    $query
      ->condition('fsd.name', $name);
  }

  // If a start timestamp is specified, filter to data after it (inclusive).
  if (!is_null($start) && is_numeric($start)) {
    $query
      ->condition('fsd.timestamp', $start, '>=');
  }

  // If an end timestamp is specified, filter to data before it (inclusive).
  if (!is_null($end) && is_numeric($end)) {
    $query
      ->condition('fsd.timestamp', $end, '<=');
  }

  // Order by timestamp descending.
  $query
    ->orderBy('fsd.timestamp', 'DESC');

  // Limit the results, if $limit is not empty.
  if (!empty($limit)) {
    $query
      ->range($offset, $limit);
  }

  // Run the query.
  $result = $query
    ->execute();

  // Build an array of data.
  $data = array();
  foreach ($result as $row) {

    // If name or timestamp are empty, skip.
    if (empty($row->timestamp) || empty($row->name)) {
      continue;
    }

    // Convert the value numerator and denominator to a decimal.
    $value = fraction($row->value_numerator, $row->value_denominator)
      ->toDecimal(0, TRUE);

    // Create a data object for the sensor value.
    $point = new stdClass();
    $point->timestamp = $row->timestamp;
    $point->{$row->name} = $value;
    $data[] = $point;
  }

  // Return the data.
  return $data;
}

/**
 * Fetch summary info about sensor values from the database.
 *
 * @param $id
 *  The sensor asset id.
 *
 * @return array
 *  Returns an array of info for each distinct value keyed by value name.
 *    'first': timestamp of data first recorded for this value on the sensor.
 *    'last': timestamp of data last recorded for this value on the sensor.
 */
function farm_sensor_listener_values_info($id) {

  // Build array of values.
  $values = array();

  // Query all the distinct value names this sensor has stored.
  $query = db_select('farm_sensor_data')
    ->fields('farm_sensor_data', array(
    'name',
  ))
    ->condition('farm_sensor_data.id', $id)
    ->groupBy('farm_sensor_data.name');

  // Select the total record count for each value name.
  $query
    ->addExpression('COUNT(farm_sensor_data.timestamp)', 'count');

  // Select the max timestamp for each value name.
  $query
    ->addExpression('MAX(farm_sensor_data.timestamp)', 'last');

  // Select the min timestamp for each value name.
  $query
    ->addExpression('MIN(farm_sensor_data.timestamp)', 'first');

  // Execute query.
  $result = $query
    ->execute();

  // Add each value's info keyed by value name.
  foreach ($result as $row) {
    $values[$row->name] = array(
      'count' => $row->count,
      'first' => $row->first,
      'last' => $row->last,
    );
  }

  // Return values.
  return $values;
}

/**
 * Implements hook_farm_sensor_type_info().
 */
function farm_sensor_listener_farm_sensor_type_info() {
  return array(
    'listener' => array(
      'label' => t('Listener'),
      'description' => t('Open up a data listener that accepts data from
      external sources over HTTP'),
      'form' => 'farm_sensor_listener_settings_form',
    ),
  );
}

/**
 * Settings form for listener sensor.
 *
 * @param FarmAsset $sensor
 *   The sensor asset entity.
 * @param array $settings
 *   The farm sensor settings.
 *
 * @return array
 *   Returns a form with settings for this  Listener sensor.
 */
function farm_sensor_listener_settings_form($sensor, $settings = array()) {

  // If a public/private key haven't been set yet, generate them.
  if (empty($settings['public_key'])) {
    $settings['public_key'] = hash('md5', mt_rand());
  }
  if (empty($settings['private_key'])) {
    $settings['private_key'] = hash('md5', mt_rand());
  }

  // Automatically generated public key.
  $form['public_key'] = array(
    '#type' => 'textfield',
    '#title' => t('Public key'),
    '#description' => t('An automatically generated public key for this sensor.'),
    '#default_value' => $settings['public_key'],
  );

  // Automatically generated private key.
  $form['private_key'] = array(
    '#type' => 'textfield',
    '#title' => t('Private key'),
    '#description' => t('An automatically generated private key for this sensor.'),
    '#default_value' => $settings['private_key'],
  );

  // Allow public access to sensor data.
  $form['public_api'] = array(
    '#type' => 'checkbox',
    '#title' => t('Allow public API read access'),
    '#description' => t('Checking this box will allow data from this sensor to be queried publicly via the API endpoint without a private key. See the <a href="@sensor_url">farmOS sensor guide</a> for more information.', array(
      '@sensor_url' => 'https://farmOS.org/guide/assets/sensors',
    )),
    '#default_value' => isset($settings['public_api']) ? $settings['public_api'] : FALSE,
  );

  // Reminder to save the sensor entity before posting data.
  if (empty($sensor->id)) {
    $form['reminder'] = array(
      '#type' => 'markup',
      '#markup' => '<p><strong>Remember to save your sensor before attempting to send data to it!</strong></p>',
    );
  }

  // Display some information about how to stream data to the listener.
  global $base_url;
  $form['info'] = array(
    '#type' => 'fieldset',
    '#title' => t('Developer Information'),
    '#description' => t('This sensor type will listen for data posted to it
    from other web-connected devices. Use the information below to configure your
    device to begin posting data to this sensor. For more information, refer to
    the <a href="@sensor_url">farmOS sensor guide</a>.', array(
      '@sensor_url' => 'https://farmOS.org/guide/assets/sensors',
    )),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );

  // URL to post data to.
  $url = $base_url . '/farm/sensor/listener/' . $settings['public_key'] . '?private_key=' . $settings['private_key'];
  $form['info']['url'] = array(
    '#type' => 'markup',
    '#markup' => '<p><strong>URL:</strong> ' . $url . '</p>',
  );

  // Example JSON objects.
  $json_example1 = '{ "timestamp": ' . REQUEST_TIME . ', "value": 76.5 }';
  $json_example2 = '{ "timestamp": ' . REQUEST_TIME . ', "sensor1": 76.5, "sensor2": 60 }';
  $form['info']['json'] = array(
    '#type' => 'markup',
    '#markup' => '<p><strong>JSON Example:</strong> ' . $json_example1 . '</p><p><strong>JSON Example (multiple values):</strong> ' . $json_example2 . '</p>',
  );

  // Example CURL command.
  $curl_example = 'curl -H "Content-Type: application/json" -X POST -d \'' . $json_example1 . '\' ' . $url;
  $form['info']['curl'] = array(
    '#type' => 'markup',
    '#markup' => '<p><strong>Example CURL command:</strong> ' . $curl_example . '</p>',
  );

  // Provide configuration for generating notifications.
  $form['notifications'] = array(
    '#type' => 'fieldset',
    '#title' => t('Notifications'),
    '#description' => t('Configure automatic email notifications when values
    are above/below a certain value. Notifications will be sent each time that
    the threshold condition defined below is crossed.'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['notifications'][0]['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Name'),
    '#description' => t('The name given to the data point in the JSON array.'),
    '#default_value' => !empty($settings['notifications'][0]['name']) ? $settings['notifications'][0]['name'] : 'value',
  );
  $form['notifications'][0]['condition'] = array(
    '#type' => 'select',
    '#title' => t('Condition'),
    '#options' => array(
      '>' => 'is greater than',
      '<' => 'is less than',
    ),
    '#default_value' => !empty($settings['notifications'][0]['condition']) ? $settings['notifications'][0]['condition'] : '>',
  );
  $form['notifications'][0]['threshold'] = array(
    '#type' => 'textfield',
    '#title' => t('Value threshold'),
    '#description' => t('If the data value is above/below this threshold, a notification will be sent.'),
    '#default_value' => !empty($settings['notifications'][0]['threshold']) ? $settings['notifications'][0]['threshold'] : '',
  );
  $form['notifications'][0]['email'] = array(
    '#type' => 'textfield',
    '#title' => t('Email address'),
    '#description' => t('Email address(es) to send notification to. Separate multiple addresses with a comma.'),
    '#default_value' => !empty($settings['notifications'][0]['email']) ? $settings['notifications'][0]['email'] : '',
    '#maxlength' => 256,
  );
  return $form;
}

/**
 * Farm Sensor Listener Data Graphs Form.
 */
function farm_sensor_listener_data_graphs_form($form, &$form_state, $asset) {

  // Bail if not a sensor asset.
  if ($asset->type != 'sensor') {
    return array();
  }

  // Fieldset to display sensor data.
  $form['data'] = array(
    '#type' => 'fieldset',
    '#title' => t('Sensor graphs'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );

  // Fieldset for query filters.
  $form['data']['filters'] = array(
    '#type' => 'fieldset',
    '#title' => t('Filters'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );

  // Load distinct sensor value info.
  $values = farm_sensor_listener_values_info($asset->id);

  // Don't display the graph fieldset if the sensor has no data.
  if (empty($values)) {
    return array();
  }

  // Load the distinct value names this sensor has stored.
  $names = array_keys($values);

  // Sensor values to display.
  // Default to all distinct values the sensor has stored.
  $form['data']['filters']['values'] = array(
    '#type' => 'select',
    '#title' => t('Sensor Values'),
    '#options' => drupal_map_assoc($names),
    '#multiple' => TRUE,
    '#default_value' => $names,
  );

  // Provide a default date in the format YYYY-MM-DD HH-MM-SS.
  $format = 'Y-m-d H:i:s';

  // Get the latest timestamp data from saved data
  // or default to current time.
  $latest = REQUEST_TIME;
  if (!empty($values)) {
    $latest = max(array_column($values, 'last'));
  }

  // Build latest date.
  $latest_date = date($format, $latest);

  // Start date. Defaults to 1 week before latest date..
  $past_week_date = date($format, strtotime('- 7 days', $latest));
  $form['data']['filters']['start_date'] = array(
    '#type' => 'date_select',
    '#title' => t('Start date'),
    '#default_value' => $past_week_date,
    '#date_year_range' => '-10:+1',
    '#date_format' => $format,
    '#date_label_position' => 'within',
  );

  // End date. Defaults to current time.
  $form['data']['filters']['end_date'] = array(
    '#type' => 'date_select',
    '#title' => t('End date'),
    '#default_value' => $latest_date,
    '#date_year_range' => '-10:+1',
    '#date_format' => $format,
    '#date_label_position' => 'within',
  );

  // Submit button.
  $form['data']['filters']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );

  // Load filter values from form state or form field #default_value.
  $filters = array(
    'values' => isset($form_state['values']['values']) ? $form_state['values']['values'] : $form['data']['filters']['values']['#default_value'],
    'start_date' => isset($form_state['values']['start_date']) ? $form_state['values']['start_date'] : $form['data']['filters']['start_date']['#default_value'],
    'end_date' => isset($form_state['values']['end_date']) ? $form_state['values']['end_date'] : $form['data']['filters']['end_date']['#default_value'],
  );

  // Iterate through the names, load the data values for each,
  // generate markup DIV ids, and store it all in JS settings.
  $markup = array();
  $graphs = array();
  foreach ($filters['values'] as $name) {
    $id = drupal_html_id('sensor-data-' . $name);

    // Load data.
    $data = farm_sensor_listener_data($asset->id, $name, strtotime($filters['start_date']), strtotime($filters['end_date']), 0);

    // Don't render a graph if there is no data to display.
    if (empty($data)) {
      $markup[] = '<div class="farm-sensor-graph"><p>' . t('No data for "@value" in this date range.', array(
        '@value' => $name,
      )) . '</p></div>';
      continue;
    }

    // Build graph markup.
    $markup[] = '<div id="' . $id . '" class="farm-sensor-graph"></div>';

    // Build graph settings.
    $graph = array(
      'name' => $name,
      'id' => $id,
      'data' => $data,
    );
    $graphs[] = $graph;
  }

  // Add Javascript and CSS to build the graphs.
  $settings = array(
    'farm_sensor_listener' => array(
      'graphs' => $graphs,
    ),
  );
  drupal_add_js($settings, 'setting');
  drupal_add_js(drupal_get_path('module', 'farm_sensor_listener') . '/farm_sensor_listener.js');
  drupal_add_js('https://cdn.plot.ly/plotly-latest.min.js', 'external');
  drupal_add_css(drupal_get_path('module', 'farm_sensor_listener') . '/farm_sensor_listener.css');

  // Output graphs.
  $form['data']['graphs'] = array(
    '#markup' => '<div class="farm-sensor-graphs clearfix">' . implode('', $markup) . '</div>',
  );
  return $form;
}

/**
 * Submit callback for farm_sensor_listener_data_form_submit.
 */
function farm_sensor_listener_data_graphs_form_submit($form, &$form_state) {

  // Rebuild the form in order to keep filter values.
  $form_state['rebuild'] = TRUE;
}

/**
 * Helper function for loading a sensor asset from it's public/private key.
 *
 * @param $key
 *   The sensor public/private key.
 *
 * @return FarmAsset|bool
 *   Returns a farm sensor asset, or FALSE if not found.
 */
function farm_sensor_listener_load($key) {

  // Query the {farm_sensor} table to look for a sensor with a matching key.
  $sql = 'SELECT id FROM {farm_sensor} WHERE settings LIKE :settings';
  $result = db_query($sql, array(
    ':settings' => '%' . db_like($key) . '%',
  ));
  $asset_id = $result
    ->fetchField();

  // If no asset id was found, bail.
  if (empty($asset_id)) {
    return FALSE;
  }

  // Attempt to load the sensor asset.
  $asset = farm_asset_load($asset_id);

  // If a sensor wasn't loaded, bail.
  if (empty($asset)) {
    return FALSE;
  }

  // Return the sensor asset.
  return $asset;
}

/**
 * Implements hook_farm_ui_entity_views().
 */
function farm_sensor_listener_farm_ui_entity_views($entity_type, $bundle, $entity) {

  // Show sensor data View on listener sensors.
  $views = array();
  if ($entity_type == 'farm_asset' && $bundle == 'sensor' && $entity->sensor_type == 'listener') {
    $views[] = array(
      'name' => 'farm_sensor_data',
      'group' => 'sensor_data',
      'always' => TRUE,
    );
  }
  return $views;
}

/**
 * Implements hook_farm_sensor_view().
 */
function farm_sensor_listener_farm_sensor_view($asset) {

  // Start build array.
  $build = array();

  // If the sensor is not a listener, bail.
  if (empty($asset->sensor_type) || $asset->sensor_type != 'listener') {
    return array();
  }

  // Load the sensor data graph form.
  $build['views']['data'] = drupal_get_form('farm_sensor_listener_data_graphs_form', $asset);

  // Return build array.
  return $build;
}

Functions

Namesort descending Description
farm_sensor_listener_check_condition Threshold condition check.
farm_sensor_listener_data Fetch sensor data from the database.
farm_sensor_listener_data_graphs_form Farm Sensor Listener Data Graphs Form.
farm_sensor_listener_data_graphs_form_submit Submit callback for farm_sensor_listener_data_form_submit.
farm_sensor_listener_entity_delete Implements hook_entity_delete().
farm_sensor_listener_farm_sensor_type_info Implements hook_farm_sensor_type_info().
farm_sensor_listener_farm_sensor_view Implements hook_farm_sensor_view().
farm_sensor_listener_farm_ui_entity_views Implements hook_farm_ui_entity_views().
farm_sensor_listener_load Helper function for loading a sensor asset from it's public/private key.
farm_sensor_listener_mail Implements hook_mail().
farm_sensor_listener_menu Implements hook_menu().
farm_sensor_listener_page_callback Callback function for processing GET and POST requests to a listener. Handles receiving JSON over HTTP and storing data to the {farm_sensor_data} table. Serves data back via API requests with optional parameters.
farm_sensor_listener_process_data Process data posted to a sensor.
farm_sensor_listener_process_notifications Process sensor notifications.
farm_sensor_listener_settings_form Settings form for listener sensor.
farm_sensor_listener_values_info Fetch summary info about sensor values from the database.