diff --git a/README.md b/README.md index 962c1b3..b988c5d 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ https://example.com/path/to/api.php?action=device-data-bounds&device-id=11 ## Notes - Readings are taken every 6 minutes as standard. + ## Contributing Contributions are welcome - feel free to [open an issue](https://github.com/ConnectedHumber/Air-Quality-Web/issues/new) or (even better) a [pull request](https://github.com/ConnectedHumber/Air-Quality-Web/compare). diff --git a/logic/Actions/DeviceData.php b/logic/Actions/DeviceData.php new file mode 100644 index 0000000..c84ebcf --- /dev/null +++ b/logic/Actions/DeviceData.php @@ -0,0 +1,103 @@ +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_datetime("start"); + $this->validator->is_datetime("end"); + $this->validator->run(); + + if(!empty($_GET["average-seconds"]) && intval($_GET["average-seconds"]) == 0) { + $this->sender->send_error_plain( + 400, "Error: That average-seconds value is invalid (an integer greater than 0 required).", [ + [ "x-time-taken", PerfFormatter::format_perf_data($start_time, $start_handle, null) ] + ] + ); + return false; + } + + if(!$this->type_repo->is_valid_type($_GET["reading-type"])) { + $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_readings_by_device( + intval($_GET["device-id"]), + $_GET["reading-type"], + new \DateTime($_GET["start"]), + new \DateTime($_GET["end"]), + !empty($_GET["average-seconds"]) ? intval($_GET["average-seconds"]) : 1 + ); + + // 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 or it doesn't exist."); + return false; + } + + + // 3: Serialise data + $start_encode = microtime(true); + $response = json_encode($data); + + + // 4: Send response + header("content-length: " . strlen($response)); + header("content-type: application/json"); + header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, $start_encode)); + echo($response); + return true; + } +} diff --git a/logic/Actions/FetchData.php b/logic/Actions/FetchData.php index 11b57e1..9f89a08 100644 --- a/logic/Actions/FetchData.php +++ b/logic/Actions/FetchData.php @@ -13,9 +13,9 @@ use \AirQuality\PerfFormatter; class FetchData implements IAction { /** @var TomlConfig */ private $settings; + /** @var IMeasurementDataRepository */ private $measurement_repo; - /** @var IMeasurementTypeRepository */ private $type_repo; diff --git a/logic/Database.php b/logic/Database.php index 0d3a00a..3eb8bea 100644 --- a/logic/Database.php +++ b/logic/Database.php @@ -42,7 +42,7 @@ class Database } public function query($sql, $variables = []) { - error_log("[Database/SQL] $sql"); + //error_log("[Database/SQL] $sql"); // FUTURE: Optionally cache prepared statements? $statement = $this->connection->prepare($sql); $statement->execute($variables); diff --git a/logic/Repositories/IMeasurementDataRepository.php b/logic/Repositories/IMeasurementDataRepository.php index 0724dc9..27bdf77 100644 --- a/logic/Repositories/IMeasurementDataRepository.php +++ b/logic/Repositories/IMeasurementDataRepository.php @@ -24,8 +24,9 @@ interface IMeasurementDataRepository { * @param string $reading_type The reading type to fetch. * @param DateTime $start The starting DateTime. * @param DateTime $end The ending DateTime. + * @param int $average_seconds The number of seconds to averageg the data over. For example a value of 3600 (1 hour) will return 1 data point per hour, with the value of each point an average of all the readings for that hour. * @return array The requested data. */ - public function get_readings_by_device(int $device_id, string $reading_type, \DateTime $start, \DateTime $end); + public function get_readings_by_device(int $device_id, string $reading_type, \DateTime $start, \DateTime $end, int $average_seconds = 1); } diff --git a/logic/Repositories/MariaDBMeasurementDataRepository.php b/logic/Repositories/MariaDBMeasurementDataRepository.php index ba1f263..1fb1c1a 100644 --- a/logic/Repositories/MariaDBMeasurementDataRepository.php +++ b/logic/Repositories/MariaDBMeasurementDataRepository.php @@ -108,12 +108,46 @@ class MariaDBMeasurementDataRepository implements IMeasurementDataRepository { )) AS end FROM {$s("table_name_metadata")} WHERE {$s("table_name_metadata")}.{$s("column_metadata_device_id")} = :device_id;", [ - $device_id + "device_id" => $device_id ] )->fetch(); } - public function get_readings_by_device(int $device_id, string $reading_type, \DateTime $start, \DateTime $end) { - + public function get_readings_by_device(int $device_id, string $reading_type, \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."); + $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 + COALESCE( + {$s("table_name_metadata")}.{$s("column_metadata_recordedon")}, + {$s("table_name_metadata")}.{$s("column_metadata_storedon")} + ) >= :start_datetime AND + COALESCE( + {$s("table_name_metadata")}.{$s("column_metadata_recordedon")}, + {$s("table_name_metadata")}.{$s("column_metadata_storedon")} + ) <= :end_datetime + GROUP BY CEIL(UNIX_TIMESTAMP(COALESCE( + {$s("table_name_metadata")}.{$s("column_metadata_recordedon")}, + {$s("table_name_metadata")}.{$s("column_metadata_storedon")} + )) / :average_seconds);", [ + "device_id" => $device_id, + "start_datetime" => $start->format(\DateTime::ISO8601), + "end_datetime" => $end->format(\DateTime::ISO8601), + "average_seconds" => $average_seconds + ] + )->fetchAll(); } }