904 lines
28 KiB
Plaintext
904 lines
28 KiB
Plaintext
<?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;
|
|
}
|
|
|
|
// If this is a GET request, retrieve sensor data.
|
|
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;
|
|
}
|
|
|
|
// Return success and do nothing on all other request types.
|
|
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'];
|
|
}
|
|
|
|
// Otherwise, try converting it from a string. If that doesn't work, we
|
|
// throw it out and fall back on REQUEST_TIME set above.
|
|
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 ((float) $value > (float) $threshold) {
|
|
$pass = TRUE;
|
|
}
|
|
break;
|
|
|
|
// Less than threshold.
|
|
case '<':
|
|
if ((float) $value < (float) $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://v1.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://v1.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;
|
|
}
|