I've had quite a few requests about how to display sensor history with a graph, so I decided to just post an example here.
I had to make some compromises regarding design/CSS, to be able to squash it all into only 2 PHP files. The important parts are not affected by that anyway though, and I've done my best to keep it short and safe/secure.
Screenshot: Requirements:
PHP web server,
At least 1 temperature sensor with history logging enabled
Included:
Bootstrap 4, Chartist JS, JQuery
sensorhistory.php:
Code: Select all
<?php
if(!isset($_SESSION)) {
session_start();
}
if(isset($_POST['oauth_consumerKey']) && isset($_POST['oauth_privateKey']) && isset($_POST['oauth_token']) && isset($_POST['oauth_tokenSecret'])) {
$_SESSION['oauth_consumerKey'] = $_POST['oauth_consumerKey'];
$_SESSION['oauth_privateKey'] = $_POST['oauth_privateKey'];
$_SESSION['oauth_token'] = $_POST['oauth_token'];
$_SESSION['oauth_tokenSecret'] = $_POST['oauth_tokenSecret'];
}
$formAction = "https://" . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'];
if(!isset($_SESSION['oauth_consumerKey']) && !isset($_SESSION['oauth_privateKey'])) {
echo "Please visit <a href='http://api.telldus.com/keys/generatePrivate' target='_blank'>this page</a>, and fill out this form accordingly:<br /><br />";
echo "<form action='" . $formAction . "' method='post'>
<label for 'oauth_consumerKey'>Public Key:</label>
<input type='text' name='oauth_consumerKey'><br /><br />
<label for 'oauth_privateKey'>Private Key:</label>
<input type='text' name='oauth_privateKey'><br /><br />
<label for 'oauth_token'>Token:</label>
<input type='text' name='oauth_token'><br /><br />
<label for 'oauth_tokenSecret'>Token Secret:</label>
<input type='text' name='oauth_tokenSecret'><br /><br />
<input type='submit' value='Send'>
</form>";
} else {
class ZNetSensors
{
const API_URL = "https://pa-api.telldus.com/json";
const REQUEST_TIMEOUT = 5,
REQUEST_USER_AGENT = 'Sensor History',
REQUEST_RETRIES = 3,
REQUEST_RETRY_SLEEP = 3;
public function __construct()
{
echo "This class is not intended to be instanciated.";
}
protected static function request($pathquery, $retry=self::REQUEST_RETRIES)
{
$oauth_consumerKey = $_SESSION['oauth_consumerKey'];
$oauth_privateKey = $_SESSION['oauth_privateKey'] ;
$oauth_token = $_SESSION['oauth_token'];
$oauth_tokenSecret = $_SESSION['oauth_tokenSecret'];
$oauth_signature = ($oauth_privateKey . "%26" . $oauth_tokenSecret);
$oauth_nonce = md5(uniqid(mt_rand(), true));
$oauth_timestamp = $_SERVER['REQUEST_TIME'];
$host=parse_url(self::API_URL, PHP_URL_HOST);
$finalurl=sprintf("%s%s", self::API_URL, $pathquery);
$options=stream_context_create(
array('http'=>
array(
'timeout' => self::REQUEST_TIMEOUT,
'method'=> 'GET',
'header'=>
"User-Agent: ".self::REQUEST_USER_AGENT."\r\n".
"Authorization: OAuth oauth_consumer_key=" . $oauth_consumerKey . ", oauth_nonce=" . $oauth_nonce . ", oauth_signature=" . $oauth_signature .", oauth_signature_method=\"PLAINTEXT\", oauth_timestamp=" . $oauth_timestamp . ", oauth_token=" . $oauth_token . ", oauth_version=\"1.0\"",
)
)
);
// VERY IMPORTANT: Keep queries per load to a minimum!!
// Uncomment the next line to reality check:
// echo "$finalurl\n";
$json=@file_get_contents($finalurl, false, $options);
if ($http_response_header[0]!='HTTP/1.1 200 OK' and $http_response_header[0]!='HTTP/1.0 200 OK') {
print_r('Request failed with \"'.$http_response_header[0]."\", retry:$retry, url:".addslashes($finalurl));
if ($retry<self::REQUEST_RETRIES) {
sleep(self::REQUEST_RETRY_SLEEP);
return self::request($pathquery, ++$retry);
}
}
$response=json_decode($json, true);
if (json_last_error()!=JSON_ERROR_NONE) {
print_r('JSON invalid') . print_r($json);
}
return $response;
}
public static function sensorsList($includeIgnored, $includeValues, $includeUnit, $includeScale)
{
$query=array();
$query['includeIgnored']=$includeIgnored;
$query['includeValues']=$includeValues;
$query['includeUnit']=$includeUnit;
$query['includeScale']=$includeScale;
return self::request("/sensors/list?".http_build_query($query));
}
public static function sensorInfo($sensorID, $includeUnit)
{
$query=array();
$query['id']=$sensorID;
$query['includeUnit']=$includeUnit;
return self::request("/sensor/info?".http_build_query($query));
}
public static function sensorHistory($sensorID, $from, $to, $includeUnit)
{ //$from = timestamp in seconds, $to = timestamp in seconds
$query=array();
$query['id']=$sensorID;
$query['from']=$from;
$query['to']=$to;
$query['includeUnit']=$includeUnit;
return self::request("/sensor/history?".http_build_query($query));
}
}
?>
<!doctype html>
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Sensor History Example in PHP">
<meta name="author" content="©2019 Atchoo Development">
<link rel="icon" href="favicon.ico">
<title>Sensor History</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="//cdn.jsdelivr.net/chartist.js/latest/chartist.min.css">
<link href="css/chartist-plugin-tooltip.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Asap:400,700&subset=latin-ext" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-select@1.13.9/dist/css/bootstrap-select.min.css">
<script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-select@1.13.9/dist/js/bootstrap-select.min.js"></script>
<script src="//cdn.jsdelivr.net/chartist.js/latest/chartist.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartist-plugin-threshold@0.0.2/dist/chartist-plugin-threshold.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartist-plugin-axistitle@0.0.4/dist/chartist-plugin-axistitle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartist-plugin-tooltips@0.0.17/dist/chartist-plugin-tooltip.min.js"></script>
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
</script>
<style>
.ct-line.ct-threshold-above, .ct-point.ct-threshold-above, .ct-bar.ct-threshold-above {
stroke: #fe6813;
}
.ct-line.ct-threshold-below, .ct-point.ct-threshold-below, .ct-bar.ct-threshold-below {
stroke: #097e9f;
}
.ct-area.ct-threshold-above {
fill: #ff0b03;
}
.ct-area.ct-threshold-below {
fill: #151ea9;
}
</style>
</head>
<body class="bg-light">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<?php
$sensors = ZNetSensors::sensorsList(0,1,1,1);
$sensor = $sensors['sensor'];
$i = "-1";
foreach($sensor as $key=>$sens) {
$sensorName = $sens['name'];
$sensorID = $sens['id'];
$sensorInfo = ZNetSensors::sensorInfo($sensorID,'1');
$sensorName = $sensorInfo['name'];
$sensorData = $sensorInfo['data'];
$sensorProtocol = $sensorInfo['protocol'];
if(!isset($_SESSION['hp'])) {
$_SESSION['hp'] = '-12';
}
$_SESSION['historyPeriod'] = ($_SESSION['hp'] . " hours");
$fromTime = strtotime($_SESSION['historyPeriod']);
$toTime = strtotime("now");
$sensorHistory = ZNetSensors::sensorHistory($sensorID, $fromTime, $toTime, 1);
$history = $sensorHistory['history'];
$keepHistory = $sens['keepHistory'];
$i='-1';
if($keepHistory == '1') {
$logValues = array ();
$logTimes = array ();
foreach($history as $key => $hist) {
$i++;
$logTS = $hist['ts'];
$logTimes[] = date('H:i', $logTS);
$logValues[] = $hist['data'][0]['value'];
}
// print_r($logValues);
// print_r($logTimes);
// $logTimes = Array of human readable Clock times of all log entries;
// i.e. "$logTimes[2]" for the time of the 3rd entry, "$logTimes[3]" for the next.
// $logValues = Array of sensor values, corresponding with $logTimes
// Note: The reason for 2 separate arrays is just conveniency due to the following hacky combination of PHP & Javascript
?>
<div class="card text-white bg-dark rounded" style="border-width: 2px; border-color: black; padding: 12px;">
<h2 class="card-header text-white rounded text-center" style="font-family: Asap; font-weight: 700; text-transform: uppercase; font-size: 24px; padding: 30px;">Sensor History<br /><span class="text-warning"><?php echo $sensorName?></span></h2>
<div class="card-body rounded bg-light">
<div class="ct-chart-line" style="height: 360px;" id="chart"></div>
<script>
var wholelabels = [<?php foreach($logTimes as $key => $t) { echo "'".$t."', "; } ?>];
var halflabels = wholelabels.filter(function(value, index, Arr) {
return index % 10 == 0;
});
var wholeseries = [<?php foreach($logValues as $key => $v) { echo $v.", "; } ?>];
var halfseries = wholeseries.filter(function(value, index, Arr) {
return index % 10 == 0;
});
var data = {
labels: halflabels,
series: [
{meta: "Temperatur", value: halfseries}
]
};
new Chartist.Line('.ct-chart-line', data, {
axisY: {
onlyInteger: true
},
fullwidth: true,
showArea: true,
chartPadding: {
top: 20,
right: 0,
bottom: 22,
left: 20
},
plugins: [
Chartist.plugins.ctAxisTitle({
axisX: {
axisTitle: 'Time',
axisClass: 'ct-axis-title',
offset: {
x: 0,
y: 50
},
textAnchor: 'middle'
},
axisY: {
axisTitle: '\u00b0C',
axisClass: 'ct-axis-title',
offset: {
x: 0,
y: -20
},
textAnchor: 'middle',
flipTitle: false
}
}),
Chartist.plugins.tooltip({
}),
]
});
</script>
</div>
<div class="card-footer text-light bg-dark rounded text-center">
<select class="selectpicker show-tick" id="historyInterval">
<option value="-24" <?php if($_SESSION['hp'] == '-24') { echo 'selected'; }?>>Last 24 Hours</option>
<option value="-12" <?php if($_SESSION['hp'] == '-12') { echo 'selected'; }?>>Last 12 Hours</option>
<option value="-8" <?php if($_SESSION['hp'] == '-8') { echo 'selected'; }?>>Last 8 Hours</option>
<option value="-6" <?php if($_SESSION['hp'] == '-6') { echo 'selected'; }?>>Last 6 Hours</option>
<option value="-3" <?php if($_SESSION['hp'] == '-3') { echo 'selected'; }?>>Last 3 Hours</option>
</select>
<script>
$('#historyInterval').change(function() {
// console.log(this.value);
$.ajax({
type: "POST",
url: "historyOptions.php",
data: { historyPeriod: this.value },
success: function() {
location.reload()
}
});
});
</script>
</div>
</div>
<?php
}}}
?>
</div> <!-- End Full Width col -->
</div> <!-- End Row -->
</div> <!-- End Container -->
</body>
</html>
Code: Select all
<?php
if(!isset($_SESSION)) {
session_start();
}
if(isset($_POST['historyPeriod'])) {
$_SESSION['hp'] = $_POST['historyPeriod'];
}
?>
Andreas