mirror of
https://github.com/ConnectedHumber/Air-Quality-Web
synced 2024-11-22 06:23:01 +00:00
Implement new device-data-recent, but it's not working right.
This commit is contained in:
parent
7710331f1e
commit
dba984340d
7 changed files with 214 additions and 3 deletions
|
@ -115,8 +115,20 @@ https://example.com/path/to/api.php?action=device-data&device-id=18&reading-type
|
||||||
https://example.com/path/to/api.php?action=device-data&device-id=18&reading-type=PM25&start=2019-06-13T00:00:00&end=now&format=csv
|
https://example.com/path/to/api.php?action=device-data&device-id=18&reading-type=PM25&start=2019-06-13T00:00:00&end=now&format=csv
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## device-data-recent
|
||||||
|
> Gets a given number of the most recent readings for a specific device.
|
||||||
|
|
||||||
|
Parameter | Type | Meaning
|
||||||
|
--------------------|-----------|---------------------
|
||||||
|
`device-id` | int | The id of the device to get data for.
|
||||||
|
`reading-type` | string | The type of reading to obtain data for.
|
||||||
|
`count` | int | The number of recent readings to return.
|
||||||
|
`format` | string | Optional. Specifies the format that the response will be returned in. Valid values: `json`, `csv`. Default: `json`.
|
||||||
|
|
||||||
|
```
|
||||||
|
https://example.com/path/to/api.php?action=device-data-recent&device-id=21&reading-type=PM25&count=5
|
||||||
|
https://example.com/path/to/api.php?action=device-data-recent&device-id=36&reading-type=humidity&count=30
|
||||||
|
```
|
||||||
|
|
||||||
## changelog
|
## changelog
|
||||||
> Gets the changelog as a fragment of HTML.
|
> Gets the changelog as a fragment of HTML.
|
||||||
|
|
125
logic/Actions/DeviceDataRecent.php
Normal file
125
logic/Actions/DeviceDataRecent.php
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AirQuality\Actions;
|
||||||
|
|
||||||
|
use \SBRL\TomlConfig;
|
||||||
|
use \SBRL\ResponseEncoder;
|
||||||
|
use \AirQuality\Repositories\IMeasurementDataRepository;
|
||||||
|
use \AirQuality\Repositories\IMeasurementTypeRepository;
|
||||||
|
use \AirQuality\ApiResponseSender;
|
||||||
|
|
||||||
|
use \AirQuality\Validator;
|
||||||
|
use \AirQuality\PerfFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action that retrieves recent data for a device.
|
||||||
|
*/
|
||||||
|
class DeviceDataRecent implements IAction {
|
||||||
|
/** @var TomlConfig */
|
||||||
|
private $settings;
|
||||||
|
|
||||||
|
/** @var IMeasurementDataRepository */
|
||||||
|
private $measurement_repo;
|
||||||
|
/** @var IMeasurementTypeRepository */
|
||||||
|
private $type_repo;
|
||||||
|
|
||||||
|
/** @var ApiResponseSender */
|
||||||
|
private $sender;
|
||||||
|
|
||||||
|
/** @var Validator */
|
||||||
|
private $validator;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
TomlConfig $in_settings,
|
||||||
|
IMeasurementDataRepository $in_measurement_repo,
|
||||||
|
IMeasurementTypeRepository $in_type_repo,
|
||||||
|
ApiResponseSender $in_sender) {
|
||||||
|
$this->settings = $in_settings;
|
||||||
|
$this->measurement_repo = $in_measurement_repo;
|
||||||
|
$this->type_repo = $in_type_repo;
|
||||||
|
$this->sender = $in_sender;
|
||||||
|
|
||||||
|
$this->validator = new Validator($_GET);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle() : bool {
|
||||||
|
global $start_time;
|
||||||
|
|
||||||
|
$start_handle = microtime(true);
|
||||||
|
|
||||||
|
// 1: Validate params
|
||||||
|
$this->validator->is_numberish("device-id");
|
||||||
|
$this->validator->exists("reading-type");
|
||||||
|
$this->validator->is_max_length("reading-type", 256);
|
||||||
|
$this->validator->is_numberish("count");
|
||||||
|
$this->validator->is_min("count", 0);
|
||||||
|
$this->validator->is_max("count", $this->settings->get("limits.device_data_recent.max_rows"));
|
||||||
|
|
||||||
|
if(!empty($_GET["format"]))
|
||||||
|
$this->validator->is_preset_value("format", ["json", "csv"], 406);
|
||||||
|
|
||||||
|
$this->validator->run();
|
||||||
|
|
||||||
|
$format = $_GET["format"] ?? "json";
|
||||||
|
|
||||||
|
$count = intval($_GET["count"]);
|
||||||
|
|
||||||
|
$reading_type_id = $this->type_repo->get_id($_GET["reading-type"]);
|
||||||
|
|
||||||
|
if($reading_type_id == null) {
|
||||||
|
$this->sender->send_error_plain(
|
||||||
|
400, "Error: That reading type is invalid.", [
|
||||||
|
[ "x-time-taken", PerfFormatter::format_perf_data($start_time, $start_handle, null) ]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2: Pull data from database
|
||||||
|
$data = $this->measurement_repo->get_recent_readings(
|
||||||
|
intval($_GET["device-id"]),
|
||||||
|
$reading_type_id,
|
||||||
|
$count
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2.5: Validate data from database
|
||||||
|
if(empty($data)) {
|
||||||
|
http_response_code(404);
|
||||||
|
header("content-type: text/plain");
|
||||||
|
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, null));
|
||||||
|
echo("Error: No data has been recorded from the device id for that measurement type in the selected time scale or it doesn't exist.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 3: Serialise data
|
||||||
|
|
||||||
|
// FUTURE: Refactor this into an AbstractAction or something, but the careful not to shift too much work there
|
||||||
|
// We might want to consider a HttpResponse container or something if we can find a decent PSR implementation
|
||||||
|
$start_encode = microtime(true);
|
||||||
|
$response_type = "application/octet-stream";
|
||||||
|
$response_suggested_filename = "data-" . date(\DateTime::ATOM) . "";
|
||||||
|
$response = null;
|
||||||
|
switch($format) {
|
||||||
|
case "json":
|
||||||
|
$response_type = "application/json";
|
||||||
|
$response_suggested_filename .= ".json";
|
||||||
|
$response = json_encode($data);
|
||||||
|
break;
|
||||||
|
case "csv":
|
||||||
|
$response_type = "text/csv";
|
||||||
|
$response_suggested_filename .= ".csv";
|
||||||
|
$response = ResponseEncoder::encode_csv($data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 4: Send response
|
||||||
|
header("content-length: " . strlen($response));
|
||||||
|
header("content-type: $response_type");
|
||||||
|
header("content-disposition: inline; filename=$response_suggested_filename");
|
||||||
|
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, $start_encode));
|
||||||
|
echo($response);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,13 @@
|
||||||
|
|
||||||
namespace AirQuality\Actions;
|
namespace AirQuality\Actions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that defines the functionality of an action that requests can be routed to.
|
||||||
|
*/
|
||||||
interface IAction {
|
interface IAction {
|
||||||
|
/**
|
||||||
|
* Handles the a request for the action.
|
||||||
|
* @return bool Whether the request was handled successfully or not.
|
||||||
|
*/
|
||||||
public function handle() : bool;
|
public function handle() : bool;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,4 +29,13 @@ interface IMeasurementDataRepository {
|
||||||
*/
|
*/
|
||||||
public function get_readings_by_device(int $device_id, int $type_id, \DateTime $start, \DateTime $end, int $average_seconds = 1);
|
public function get_readings_by_device(int $device_id, int $type_id, \DateTime $start, \DateTime $end, int $average_seconds = 1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a given number of the most recent readings for a specific device.
|
||||||
|
* @param int $device_id The id of the device to get recent readings for.
|
||||||
|
* @param int $type_id The id of the reading type to fetch data for.
|
||||||
|
* @param int $count The number of readings to return.
|
||||||
|
* @return array The requested data.
|
||||||
|
*/
|
||||||
|
public function get_recent_readings(int $device_id, int $type_id, int $count);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,10 @@ class MariaDBDeviceRepository implements IDeviceRepository {
|
||||||
*/
|
*/
|
||||||
private $database;
|
private $database;
|
||||||
|
|
||||||
/** Function that gets a static variable by it's name. Useful in preparing SQL queries. */
|
/**
|
||||||
|
* Function that gets a static variable by it's name. Useful in preparing SQL queries.
|
||||||
|
* @var callable
|
||||||
|
*/
|
||||||
private $get_static;
|
private $get_static;
|
||||||
|
|
||||||
function __construct(\AirQuality\Database $in_database) {
|
function __construct(\AirQuality\Database $in_database) {
|
||||||
|
|
|
@ -35,10 +35,23 @@ class MariaDBMeasurementDataRepository implements IMeasurementDataRepository {
|
||||||
*/
|
*/
|
||||||
private $database;
|
private $database;
|
||||||
|
|
||||||
/** Function that gets a static variable by it's name. Useful in preparing SQL queries. */
|
/**
|
||||||
|
* Function that gets a static variable by it's name. Useful in preparing SQL queries.
|
||||||
|
* @var callable
|
||||||
|
*/
|
||||||
private $get_static;
|
private $get_static;
|
||||||
|
/**
|
||||||
|
* Function that gets a static variable by it's name & class. Useful in preparing SQL queries.
|
||||||
|
* @var callable
|
||||||
|
*/
|
||||||
private $get_static_extra;
|
private $get_static_extra;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new reepository instance.
|
||||||
|
* It is suggested that PHP-DI is utilised for instantiation via the associated interface.
|
||||||
|
* @param Database $in_database The database connection to use.
|
||||||
|
* @param TomlConfig $in_settings The settings to operate with.
|
||||||
|
*/
|
||||||
function __construct(Database $in_database, TomlConfig $in_settings) {
|
function __construct(Database $in_database, TomlConfig $in_settings) {
|
||||||
$this->database = $in_database;
|
$this->database = $in_database;
|
||||||
$this->settings = $in_settings;
|
$this->settings = $in_settings;
|
||||||
|
@ -52,6 +65,8 @@ class MariaDBMeasurementDataRepository implements IMeasurementDataRepository {
|
||||||
public function get_readings_by_date(\DateTime $datetime, int $type_id) {
|
public function get_readings_by_date(\DateTime $datetime, int $type_id) {
|
||||||
$s = $this->get_static;
|
$s = $this->get_static;
|
||||||
$o = $this->get_static_extra;
|
$o = $this->get_static_extra;
|
||||||
|
|
||||||
|
// OPTIMIZE: I think we can drastically improve the performance of this query by pre-calculating the start & end dates of the window
|
||||||
return $this->database->query(
|
return $this->database->query(
|
||||||
"SELECT
|
"SELECT
|
||||||
{$s("table_name_values")}.{$s("column_values_value")},
|
{$s("table_name_values")}.{$s("column_values_value")},
|
||||||
|
@ -106,7 +121,7 @@ class MariaDBMeasurementDataRepository implements IMeasurementDataRepository {
|
||||||
|
|
||||||
public function get_readings_by_device(int $device_id, int $type_id, \DateTime $start, \DateTime $end, int $average_seconds = 1) {
|
public function get_readings_by_device(int $device_id, int $type_id, \DateTime $start, \DateTime $end, int $average_seconds = 1) {
|
||||||
if($average_seconds < 1)
|
if($average_seconds < 1)
|
||||||
throw new Exception("Error: average_seconds must be greater than 1, but '$average_seconds' was specified.");
|
throw new \Exception("Error: average_seconds must be greater than 1, but '$average_seconds' was specified.");
|
||||||
$s = $this->get_static;
|
$s = $this->get_static;
|
||||||
return $this->database->query(
|
return $this->database->query(
|
||||||
"SELECT
|
"SELECT
|
||||||
|
@ -144,4 +159,35 @@ class MariaDBMeasurementDataRepository implements IMeasurementDataRepository {
|
||||||
]
|
]
|
||||||
)->fetchAll();
|
)->fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function get_recent_readings(int $device_id, int $type_id, int $count) {
|
||||||
|
if($count <= 0) return [];
|
||||||
|
|
||||||
|
$s = $this->get_static;
|
||||||
|
return $this->database->query(
|
||||||
|
"SELECT
|
||||||
|
AVG({$s("table_name_values")}.{$s("column_values_value")}) AS {$s("column_values_value")},
|
||||||
|
MIN({$s("table_name_values")}.{$s("column_values_reading_id")}) AS {$s("column_values_reading_id")},
|
||||||
|
|
||||||
|
MIN(COALESCE(
|
||||||
|
{$s("table_name_metadata")}.{$s("column_metadata_recordedon")},
|
||||||
|
{$s("table_name_metadata")}.{$s("column_metadata_storedon")}
|
||||||
|
)) AS datetime
|
||||||
|
FROM {$s("table_name_values")}
|
||||||
|
JOIN {$s("table_name_metadata")} ON
|
||||||
|
{$s("table_name_metadata")}.{$s("column_metadata_id")} = {$s("table_name_values")}.{$s("column_values_reading_id")}
|
||||||
|
WHERE
|
||||||
|
{$s("table_name_metadata")}.{$s("column_metadata_device_id")} = :device_id AND
|
||||||
|
{$s("table_name_values")}.{$s("column_values_reading_type")} = :reading_type
|
||||||
|
ORDER BY COALESCE(
|
||||||
|
{$s("table_name_metadata")}.{$s("column_metadata_recordedon")},
|
||||||
|
{$s("table_name_metadata")}.{$s("column_metadata_storedon")}
|
||||||
|
)
|
||||||
|
LIMIT :count;", [
|
||||||
|
"device_id" => $device_id,
|
||||||
|
"reading_type" => $type_id,
|
||||||
|
"count" => $count
|
||||||
|
]
|
||||||
|
)->fetchAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,3 +45,12 @@ max-age = 2592000 # 30 days
|
||||||
# time-step. Ideally, this value should be half of the actual data recording
|
# time-step. Ideally, this value should be half of the actual data recording
|
||||||
# interval.
|
# interval.
|
||||||
max_reading_timediff = 180
|
max_reading_timediff = 180
|
||||||
|
|
||||||
|
[limits]
|
||||||
|
# Various tunable limits.
|
||||||
|
|
||||||
|
[limits.device_data_recent]
|
||||||
|
# The device-data-recent action
|
||||||
|
|
||||||
|
# The max. number of rows allowed in a single request
|
||||||
|
max_rows = 1000
|
||||||
|
|
Loading…
Reference in a new issue