From 110d7c2aec84f5e9d66e23178a5bf43afd9a04ff Mon Sep 17 00:00:00 2001 From: Lloyd Date: Sat, 11 Apr 2026 20:42:04 +0100 Subject: [PATCH] feat: add airtime data retrieval functionality with API endpoint --- repeater/data_acquisition/sqlite_handler.py | 28 +++++++++++++++++++ .../data_acquisition/storage_collector.py | 8 ++++++ repeater/web/api_endpoints.py | 16 +++++++++++ 3 files changed, 52 insertions(+) diff --git a/repeater/data_acquisition/sqlite_handler.py b/repeater/data_acquisition/sqlite_handler.py index bbb3f5c..9a582be 100644 --- a/repeater/data_acquisition/sqlite_handler.py +++ b/repeater/data_acquisition/sqlite_handler.py @@ -895,6 +895,34 @@ class SQLiteHandler: logger.error(f"Failed to get filtered packets: {e}") return [] + def get_airtime_data( + self, + start_timestamp: Optional[float] = None, + end_timestamp: Optional[float] = None, + limit: int = 50000, + ) -> list: + """Lightweight query returning only columns needed for airtime charting.""" + try: + with sqlite3.connect(self.sqlite_path) as conn: + conn.row_factory = sqlite3.Row + where_clauses = [] + params: list = [] + if start_timestamp is not None: + where_clauses.append("timestamp >= ?") + params.append(start_timestamp) + if end_timestamp is not None: + where_clauses.append("timestamp <= ?") + params.append(end_timestamp) + query = "SELECT timestamp, length, payload_length, transmitted FROM packets" + if where_clauses: + query += " WHERE " + " AND ".join(where_clauses) + query += " ORDER BY timestamp DESC LIMIT ?" + params.append(limit) + return [dict(row) for row in conn.execute(query, params).fetchall()] + except Exception as e: + logger.error(f"Failed to get airtime data: {e}") + return [] + def get_packet_by_hash(self, packet_hash: str) -> Optional[dict]: try: with sqlite3.connect(self.sqlite_path) as conn: diff --git a/repeater/data_acquisition/storage_collector.py b/repeater/data_acquisition/storage_collector.py index 494aa3e..b128165 100644 --- a/repeater/data_acquisition/storage_collector.py +++ b/repeater/data_acquisition/storage_collector.py @@ -247,6 +247,14 @@ class StorageCollector: packet_type, route, start_timestamp, end_timestamp, limit, offset ) + def get_airtime_data( + self, + start_timestamp: Optional[float] = None, + end_timestamp: Optional[float] = None, + limit: int = 50000, + ) -> list: + return self.sqlite_handler.get_airtime_data(start_timestamp, end_timestamp, limit) + def get_packet_by_hash(self, packet_hash: str) -> Optional[dict]: return self.sqlite_handler.get_packet_by_hash(packet_hash) diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py index 750109e..8b27504 100644 --- a/repeater/web/api_endpoints.py +++ b/repeater/web/api_endpoints.py @@ -1343,6 +1343,22 @@ class APIEndpoints: logger.error(f"Error getting filtered packets: {e}") return self._error(e) + @cherrypy.expose + @cherrypy.tools.json_out() + def airtime_data(self, start_timestamp=None, end_timestamp=None, limit=50000): + """Lightweight endpoint returning only columns needed for airtime charting.""" + try: + start_ts = float(start_timestamp) if start_timestamp is not None else None + end_ts = float(end_timestamp) if end_timestamp is not None else None + limit_int = min(int(limit), 50000) + packets = self._get_storage().get_airtime_data( + start_timestamp=start_ts, end_timestamp=end_ts, limit=limit_int, + ) + return self._success(packets, count=len(packets)) + except Exception as e: + logger.error(f"Error getting airtime data: {e}") + return self._error(e) + @cherrypy.expose @cherrypy.tools.json_out() def packet_by_hash(self, packet_hash=None):