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 @@
+<?php
+
+namespace AirQuality\Actions;
+
+use \SBRL\TomlConfig;
+use \AirQuality\Repositories\IMeasurementDataRepository;
+use \AirQuality\Repositories\IMeasurementTypeRepository;
+use \AirQuality\ApiResponseSender;
+
+use \AirQuality\Validator;
+use \AirQuality\PerfFormatter;
+
+class DeviceData 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_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();
 	}
 }
