From dba984340d7940924c10d51c820448d5e8193d04 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Thu, 13 Jun 2019 22:02:04 +0100 Subject: [PATCH] Implement new device-data-recent, but it's not working right. --- docs/05-API-Docs.md | 12 ++ logic/Actions/DeviceDataRecent.php | 125 ++++++++++++++++++ logic/Actions/IAction.php | 7 + .../IMeasurementDataRepository.php | 9 ++ .../Repositories/MariaDBDeviceRepository.php | 5 +- .../MariaDBMeasurementDataRepository.php | 50 ++++++- settings.default.toml | 9 ++ 7 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 logic/Actions/DeviceDataRecent.php diff --git a/docs/05-API-Docs.md b/docs/05-API-Docs.md index 3a672a2..654b625 100644 --- a/docs/05-API-Docs.md +++ b/docs/05-API-Docs.md @@ -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 ``` +## 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 > Gets the changelog as a fragment of HTML. diff --git a/logic/Actions/DeviceDataRecent.php b/logic/Actions/DeviceDataRecent.php new file mode 100644 index 0000000..2468cee --- /dev/null +++ b/logic/Actions/DeviceDataRecent.php @@ -0,0 +1,125 @@ +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; + } +} diff --git a/logic/Actions/IAction.php b/logic/Actions/IAction.php index 1aef415..b136d18 100644 --- a/logic/Actions/IAction.php +++ b/logic/Actions/IAction.php @@ -2,6 +2,13 @@ namespace AirQuality\Actions; +/** + * Interface that defines the functionality of an action that requests can be routed to. + */ interface IAction { + /** + * Handles the a request for the action. + * @return bool Whether the request was handled successfully or not. + */ public function handle() : bool; } diff --git a/logic/Repositories/IMeasurementDataRepository.php b/logic/Repositories/IMeasurementDataRepository.php index 7220fb3..ad79a99 100644 --- a/logic/Repositories/IMeasurementDataRepository.php +++ b/logic/Repositories/IMeasurementDataRepository.php @@ -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); + /** + * 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); + } diff --git a/logic/Repositories/MariaDBDeviceRepository.php b/logic/Repositories/MariaDBDeviceRepository.php index 20a1d73..44a3ce1 100644 --- a/logic/Repositories/MariaDBDeviceRepository.php +++ b/logic/Repositories/MariaDBDeviceRepository.php @@ -35,7 +35,10 @@ class MariaDBDeviceRepository implements IDeviceRepository { */ 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; function __construct(\AirQuality\Database $in_database) { diff --git a/logic/Repositories/MariaDBMeasurementDataRepository.php b/logic/Repositories/MariaDBMeasurementDataRepository.php index cc9523c..e4a4d24 100644 --- a/logic/Repositories/MariaDBMeasurementDataRepository.php +++ b/logic/Repositories/MariaDBMeasurementDataRepository.php @@ -35,10 +35,23 @@ class MariaDBMeasurementDataRepository implements IMeasurementDataRepository { */ 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; + /** + * Function that gets a static variable by it's name & class. Useful in preparing SQL queries. + * @var callable + */ 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) { $this->database = $in_database; $this->settings = $in_settings; @@ -52,6 +65,8 @@ class MariaDBMeasurementDataRepository implements IMeasurementDataRepository { public function get_readings_by_date(\DateTime $datetime, int $type_id) { $s = $this->get_static; $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( "SELECT {$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) { 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; return $this->database->query( "SELECT @@ -144,4 +159,35 @@ class MariaDBMeasurementDataRepository implements IMeasurementDataRepository { ] )->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(); + } } diff --git a/settings.default.toml b/settings.default.toml index 9a71230..aced295 100644 --- a/settings.default.toml +++ b/settings.default.toml @@ -45,3 +45,12 @@ max-age = 2592000 # 30 days # time-step. Ideally, this value should be half of the actual data recording # interval. 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