View source
<?php
include_once 'farm_sensor_listener.features.inc';
function farm_sensor_listener_menu() {
$items = array();
$items['farm/sensor/listener/%'] = array(
'page callback' => 'farm_sensor_listener_page_callback',
'page arguments' => array(
3,
),
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
return $items;
}
function farm_sensor_listener_entity_delete($entity, $type) {
if ($type != 'farm_asset') {
return;
}
if ($entity->type != 'sensor') {
return;
}
if ($entity->sensor_type != 'listener') {
return;
}
db_delete('farm_sensor_data')
->condition('id', $entity->id)
->execute();
}
function farm_sensor_listener_mail($key, &$message, $params) {
if ($key == 'listener_notification') {
$message['subject'] = t('Sensor notification from @sensor', array(
'@sensor' => $params['sensor']->name,
));
$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']);
$entity_label = htmlspecialchars(entity_label('farm_asset', $params['sensor']));
$message['body'][] = $entity_label . ': ' . url($uri['path'], array(
'absolute' => TRUE,
));
}
}
function farm_sensor_listener_page_callback($public_key) {
$params = drupal_get_query_parameters();
$sensor = farm_sensor_listener_load($public_key);
if (empty($sensor)) {
return MENU_NOT_FOUND;
}
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 ($_SERVER['REQUEST_METHOD'] == 'POST') {
$data = drupal_json_decode(file_get_contents("php://input"));
if (!empty($data)) {
farm_sensor_listener_process_data($sensor, $data);
}
return MENU_FOUND;
}
elseif ($_SERVER['REQUEST_METHOD'] == 'GET') {
drupal_add_http_header('Access-Control-Allow-Origin', '*');
$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;
}
$name = '';
if (isset($params['name'])) {
$name = $params['name'];
}
$start = NULL;
if (isset($params['start'])) {
$start = $params['start'];
}
$end = NULL;
if (isset($params['end'])) {
$end = $params['end'];
}
$limit = 1;
if (isset($params['limit'])) {
$limit = $params['limit'];
$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;
}
}
$offset = 0;
if (isset($params['offset'])) {
$offset = $params['offset'];
}
$data = farm_sensor_listener_data($sensor->id, $name, $start, $end, $limit, $offset);
drupal_json_output($data);
return MENU_FOUND;
}
else {
return MENU_FOUND;
}
}
function farm_sensor_listener_process_data($sensor, $data) {
if (is_array(reset($data))) {
foreach ($data as $point) {
farm_sensor_listener_process_data($sensor, $point);
}
return;
}
$timestamp = REQUEST_TIME;
if (!empty($data['timestamp'])) {
if (is_numeric($data['timestamp'])) {
$timestamp = $data['timestamp'];
}
else {
$strtotime = strtotime($data['timestamp']);
if (!empty($strtotime)) {
$timestamp = $strtotime;
}
}
}
foreach ($data as $key => $value) {
if ($key == 'timestamp') {
continue;
}
if (!is_numeric($value)) {
continue;
}
farm_sensor_listener_process_notifications($sensor, $key, $value);
$row = array(
'id' => $sensor->id,
'timestamp' => $timestamp,
'name' => $key,
);
$fraction = fraction_from_decimal($value);
$row['value_numerator'] = $fraction
->getNumerator();
$row['value_denominator'] = $fraction
->getDenominator();
drupal_write_record('farm_sensor_data', $row);
module_invoke_all('farm_sensor_listener_data', $sensor, $key, $value);
}
}
function farm_sensor_listener_check_condition($value, $condition, $threshold) {
$pass = FALSE;
switch ($condition) {
case '>':
if ((double) $value > (double) $threshold) {
$pass = TRUE;
}
break;
case '<':
if ((double) $value < (double) $threshold) {
$pass = TRUE;
}
break;
}
return $pass;
}
function farm_sensor_listener_process_notifications($sensor, $data_name, $value) {
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'])) {
$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'];
if ($name != $data_name) {
return;
}
$pass = farm_sensor_listener_check_condition($value, $condition, $threshold);
if (!$pass) {
return;
}
if ($pass) {
$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 (isset($row['value_numerator']) && isset($row['value_denominator'])) {
$last_value = fraction($row['value_numerator'], $row['value_denominator'])
->toDecimal(0, TRUE);
if (farm_sensor_listener_check_condition($last_value, $condition, $threshold)) {
$pass = FALSE;
}
}
}
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 . ')');
}
}
}
function farm_sensor_listener_data($id, $name = '', $start = NULL, $end = NULL, $limit = 1, $offset = 0) {
$query = db_select('farm_sensor_data', 'fsd');
$query
->fields('fsd', array(
'timestamp',
'name',
'value_numerator',
'value_denominator',
));
$query
->condition('fsd.id', $id);
if (!empty($name)) {
$query
->condition('fsd.name', $name);
}
if (!is_null($start) && is_numeric($start)) {
$query
->condition('fsd.timestamp', $start, '>=');
}
if (!is_null($end) && is_numeric($end)) {
$query
->condition('fsd.timestamp', $end, '<=');
}
$query
->orderBy('fsd.timestamp', 'DESC');
if (!empty($limit)) {
$query
->range($offset, $limit);
}
$result = $query
->execute();
$data = array();
foreach ($result as $row) {
if (empty($row->timestamp) || empty($row->name)) {
continue;
}
$value = fraction($row->value_numerator, $row->value_denominator)
->toDecimal(0, TRUE);
$point = new stdClass();
$point->timestamp = $row->timestamp;
$point->{$row->name} = $value;
$data[] = $point;
}
return $data;
}
function farm_sensor_listener_values_info($id) {
$values = array();
$query = db_select('farm_sensor_data')
->fields('farm_sensor_data', array(
'name',
))
->condition('farm_sensor_data.id', $id)
->groupBy('farm_sensor_data.name');
$query
->addExpression('COUNT(farm_sensor_data.timestamp)', 'count');
$query
->addExpression('MAX(farm_sensor_data.timestamp)', 'last');
$query
->addExpression('MIN(farm_sensor_data.timestamp)', 'first');
$result = $query
->execute();
foreach ($result as $row) {
$values[$row->name] = array(
'count' => $row->count,
'first' => $row->first,
'last' => $row->last,
);
}
return $values;
}
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',
),
);
}
function farm_sensor_listener_settings_form($sensor, $settings = array()) {
if (empty($settings['public_key'])) {
$settings['public_key'] = hash('md5', mt_rand());
}
if (empty($settings['private_key'])) {
$settings['private_key'] = hash('md5', mt_rand());
}
$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'],
);
$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'],
);
$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,
);
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>',
);
}
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 = $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>',
);
$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>',
);
$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>',
);
$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;
}
function farm_sensor_listener_data_graphs_form($form, &$form_state, $asset) {
if ($asset->type != 'sensor') {
return array();
}
$form['data'] = array(
'#type' => 'fieldset',
'#title' => t('Sensor graphs'),
'#collapsible' => TRUE,
'#collapsed' => FALSE,
);
$form['data']['filters'] = array(
'#type' => 'fieldset',
'#title' => t('Filters'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$values = farm_sensor_listener_values_info($asset->id);
if (empty($values)) {
return array();
}
$names = array_keys($values);
$form['data']['filters']['values'] = array(
'#type' => 'select',
'#title' => t('Sensor Values'),
'#options' => drupal_map_assoc($names),
'#multiple' => TRUE,
'#default_value' => $names,
);
$format = 'Y-m-d H:i:s';
$latest = REQUEST_TIME;
if (!empty($values)) {
$latest = max(array_column($values, 'last'));
}
$latest_date = date($format, $latest);
$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',
);
$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',
);
$form['data']['filters']['submit'] = array(
'#type' => 'submit',
'#value' => t('Submit'),
);
$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'],
);
$markup = array();
$graphs = array();
foreach ($filters['values'] as $name) {
$id = drupal_html_id('sensor-data-' . $name);
$data = farm_sensor_listener_data($asset->id, $name, strtotime($filters['start_date']), strtotime($filters['end_date']), 0);
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;
}
$markup[] = '<div id="' . $id . '" class="farm-sensor-graph"></div>';
$graph = array(
'name' => $name,
'id' => $id,
'data' => $data,
);
$graphs[] = $graph;
}
$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');
$form['data']['graphs'] = array(
'#markup' => '<div class="farm-sensor-graphs clearfix">' . implode('', $markup) . '</div>',
);
return $form;
}
function farm_sensor_listener_data_graphs_form_submit($form, &$form_state) {
$form_state['rebuild'] = TRUE;
}
function farm_sensor_listener_load($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 (empty($asset_id)) {
return FALSE;
}
$asset = farm_asset_load($asset_id);
if (empty($asset)) {
return FALSE;
}
return $asset;
}
function farm_sensor_listener_farm_ui_entity_views($entity_type, $bundle, $entity) {
$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;
}
function farm_sensor_listener_farm_sensor_view($asset) {
$build = array();
if (empty($asset->sensor_type) || $asset->sensor_type != 'listener') {
return array();
}
$build['views']['data'] = drupal_get_form('farm_sensor_listener_data_graphs_form', $asset);
return $build;
}