66import contextlib
77import sqlite3
88import time
9+ from datetime import UTC , datetime
910from pathlib import Path
11+ from typing import Any
1012
1113from pydantic import BaseModel
1214
@@ -188,9 +190,12 @@ def store_reading(self, reading: SensorReadingSchema):
188190 def commit_batch (self ):
189191 """Commit the current batch of readings to the database."""
190192 if not self .batch_buffer :
191- return
193+ return 0
192194
193195 try :
196+ # Store count before clearing buffer
197+ count = len (self .batch_buffer )
198+
194199 # Use executemany for efficient bulk insert
195200 self .conn .executemany (
196201 """
@@ -223,6 +228,9 @@ def commit_batch(self):
223228 self .batch_buffer = []
224229 self .last_commit_time = time .time ()
225230
231+ # Return the count of committed records
232+ return count
233+
226234 except sqlite3 .Error as e :
227235 self .logger .error (f"Failed to commit batch: { e } " )
228236 raise
@@ -282,7 +290,90 @@ def __exit__(self, exc_type, exc_val, exc_tb):
282290 """Context manager exit."""
283291 self .close ()
284292
285- # Compatibility methods
293+ # Compatibility methods for tests
294+ def insert_reading (self , reading = None , ** kwargs : Any ):
295+ """Compatibility method - calls store_reading."""
296+ if reading is None and kwargs :
297+ # Create a reading from kwargs for test compatibility
298+ # Add defaults for missing fields
299+ defaults : dict [str , Any ] = {
300+ "timestamp" : datetime .now (UTC ).isoformat (),
301+ "sensor_id" : "TEST001" ,
302+ "temperature" : 0.0 ,
303+ "humidity" : 0.0 ,
304+ "pressure" : 0.0 ,
305+ "voltage" : 0.0 ,
306+ "vibration" : 0.0 ,
307+ "status_code" : 0 ,
308+ "anomaly_flag" : False ,
309+ "anomaly_type" : None ,
310+ "firmware_version" : None ,
311+ "model" : None ,
312+ "manufacturer" : None ,
313+ "location" : None ,
314+ "original_timezone" : None ,
315+ }
316+ defaults .update (kwargs )
317+ # Type conversion to ensure proper types for SensorReadingSchema
318+ reading = SensorReadingSchema (
319+ timestamp = str (defaults ["timestamp" ]),
320+ sensor_id = str (defaults ["sensor_id" ]),
321+ temperature = float (defaults ["temperature" ] or 0.0 ),
322+ humidity = float (defaults .get ("humidity" ) or 0.0 ),
323+ pressure = float (defaults .get ("pressure" ) or 0.0 ),
324+ voltage = float (defaults .get ("voltage" ) or 0.0 ),
325+ vibration = float (defaults .get ("vibration" ) or 0.0 ),
326+ status_code = int (defaults ["status_code" ] or 0 ),
327+ anomaly_flag = bool (defaults ["anomaly_flag" ]),
328+ anomaly_type = str (defaults .get ("anomaly_type" ))
329+ if defaults .get ("anomaly_type" )
330+ else None ,
331+ firmware_version = str (defaults .get ("firmware_version" ))
332+ if defaults .get ("firmware_version" )
333+ else None ,
334+ model = str (defaults .get ("model" )) if defaults .get ("model" ) else None ,
335+ manufacturer = str (defaults .get ("manufacturer" ))
336+ if defaults .get ("manufacturer" )
337+ else None ,
338+ location = str (defaults .get ("location" )) if defaults .get ("location" ) else None ,
339+ original_timezone = str (defaults .get ("original_timezone" ))
340+ if defaults .get ("original_timezone" )
341+ else None ,
342+ )
343+ return self .store_reading (reading )
344+
345+ def is_healthy (self ) -> bool :
346+ """Check if database connection is healthy."""
347+ try :
348+ if not self .conn :
349+ return False
350+ cursor = self .conn .cursor ()
351+ cursor .execute ("SELECT 1" )
352+ cursor .fetchone ()
353+ return True
354+ except Exception :
355+ return False
356+
357+ def get_readings (self , limit : int | None = None ) -> list :
358+ """Get sensor readings from the database."""
359+ try :
360+ cursor = self .conn .cursor ()
361+ query = "SELECT * FROM sensor_readings ORDER BY timestamp DESC"
362+ if limit :
363+ query += f" LIMIT { limit } "
364+ cursor .execute (query )
365+
366+ columns = [desc [0 ] for desc in cursor .description ]
367+ rows = cursor .fetchall ()
368+ return [dict (zip (columns , row , strict = False )) for row in rows ]
369+ except Exception as e :
370+ self .logger .error (f"Failed to get readings: { e } " )
371+ return []
372+
373+ def get_reading_stats (self ) -> dict :
374+ """Get reading statistics (compatibility method)."""
375+ return self .get_database_stats ()
376+
286377 def stop_background_commit_thread (self ):
287378 """No-op for compatibility."""
288379 pass
@@ -297,11 +388,52 @@ def get_database_stats(self) -> dict:
297388 cursor .execute ("SELECT COUNT(*) FROM sensor_readings WHERE anomaly_flag = 1" )
298389 anomalies = cursor .fetchone ()[0 ]
299390
391+ # Calculate database size
392+ db_size_mb = 0.0
393+ if self .db_path != ":memory:" and Path (self .db_path ).exists ():
394+ db_size = Path (self .db_path ).stat ().st_size
395+ db_size_mb = float (db_size ) / (1024 * 1024 ) # Convert to MB
396+ elif self .db_path != ":memory:" and total > 0 :
397+ # For file database when file doesn't exist yet, estimate based on records
398+ # Rough estimate: ~200 bytes per record
399+ db_size_mb = float (total * 200 ) / (1024 * 1024 )
400+ # For in-memory database, size stays 0
401+
402+ # Calculate performance metrics
403+ total_batches = getattr (self , "_commit_count" , 0 )
404+ total_inserts = total
405+ avg_batch_size = total_inserts / max (total_batches , 1 )
406+
300407 return {
301408 "total_readings" : total ,
302409 "anomaly_count" : anomalies ,
303- "database_size" : 0 , # Not calculated for simplicity
410+ "database_size" : db_size_mb * 1024 * 1024 , # In bytes for backward compat
411+ "database_size_mb" : db_size_mb ,
412+ "sensor_stats" : {}, # Not tracking individual sensor stats
413+ "anomaly_stats" : {
414+ "total" : anomalies ,
415+ "percentage" : (anomalies / max (total , 1 )) * 100 ,
416+ },
417+ "performance_metrics" : {
418+ "total_batches" : total_batches ,
419+ "total_inserts" : total_inserts ,
420+ "avg_batch_size" : avg_batch_size ,
421+ "avg_insert_time_ms" : 0.0 , # Not tracking this
422+ },
304423 }
305424 except Exception as e :
306425 self .logger .error (f"Failed to get database stats: { e } " )
307- return {"total_readings" : 0 , "anomaly_count" : 0 , "database_size" : 0 }
426+ return {
427+ "total_readings" : 0 ,
428+ "anomaly_count" : 0 ,
429+ "database_size" : 0 ,
430+ "database_size_mb" : 0 ,
431+ "sensor_stats" : {},
432+ "anomaly_stats" : {"total" : 0 , "percentage" : 0 },
433+ "performance_metrics" : {
434+ "total_batches" : 0 ,
435+ "total_inserts" : 0 ,
436+ "avg_batch_size" : 0 ,
437+ "avg_insert_time_ms" : 0.0 ,
438+ },
439+ }
0 commit comments