use anyhow::{Context, Result}; use rusqlite::Connection; use crate::types::{DoorStatus, HistoryEntry, StatusResponse}; pub fn init(path: &str) -> Result { let conn = Connection::open(path).context("failed to open SQLite database")?; conn.execute_batch( " CREATE TABLE IF NOT EXISTS current_state ( id INTEGER PRIMARY KEY CHECK (id = 1), status TEXT, timestamp INTEGER, last_seen INTEGER ); CREATE TABLE IF NOT EXISTS state_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, status TEXT NOT NULL, timestamp INTEGER NOT NULL, recorded_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS pi_info ( id INTEGER PRIMARY KEY CHECK (id = 1), data TEXT NOT NULL, fetched_at INTEGER NOT NULL ); INSERT OR IGNORE INTO current_state (id, status, timestamp, last_seen) VALUES (1, NULL, NULL, NULL); INSERT OR IGNORE INTO pi_info (id, data, fetched_at) VALUES (1, '{}', 0); ", ) .context("failed to initialize database schema")?; Ok(conn) } pub fn get_status(conn: &Connection) -> Result { let (status, timestamp, last_seen) = conn.query_row( "SELECT status, timestamp, last_seen FROM current_state WHERE id = 1", [], |row| { Ok(( row.get::<_, Option>(0)?, row.get::<_, Option>(1)?, row.get::<_, Option>(2)?, )) }, )?; Ok(StatusResponse { status: status.unwrap_or_else(|| "offline".to_string()), timestamp, last_seen, }) } pub fn update_state(conn: &Connection, status: DoorStatus, timestamp: u64, now: u64) -> Result<()> { let status_str = status.as_str(); conn.execute( "UPDATE current_state SET status = ?1, timestamp = ?2, last_seen = ?3 WHERE id = 1", rusqlite::params![status_str, timestamp, now], )?; conn.execute( "INSERT INTO state_log (status, timestamp, recorded_at) VALUES (?1, ?2, ?3)", rusqlite::params![status_str, timestamp, now], )?; Ok(()) } pub fn update_last_seen(conn: &Connection, now: u64) -> Result<()> { conn.execute( "UPDATE current_state SET last_seen = ?1 WHERE id = 1", rusqlite::params![now], )?; Ok(()) } pub fn mark_offline(conn: &Connection, now: u64) -> Result<()> { conn.execute( "UPDATE current_state SET status = NULL WHERE id = 1", [], )?; conn.execute( "INSERT INTO state_log (status, timestamp, recorded_at) VALUES ('offline', ?1, ?1)", rusqlite::params![now], )?; Ok(()) } pub fn get_current_status_str(conn: &Connection) -> Result> { let status = conn.query_row( "SELECT status FROM current_state WHERE id = 1", [], |row| row.get::<_, Option>(0), )?; Ok(status) } pub fn get_history(conn: &Connection, limit: u32) -> Result> { let mut stmt = conn.prepare( "SELECT status, timestamp, recorded_at FROM state_log ORDER BY id DESC LIMIT ?1", )?; let entries = stmt .query_map(rusqlite::params![limit], |row| { Ok(HistoryEntry { status: row.get(0)?, timestamp: row.get(1)?, recorded_at: row.get(2)?, }) })? .collect::, _>>()?; Ok(entries) } pub fn get_pi_info(conn: &Connection) -> Result { let data: String = conn.query_row( "SELECT data FROM pi_info WHERE id = 1", [], |row| row.get(0), )?; Ok(serde_json::from_str(&data).unwrap_or(serde_json::json!({}))) } pub fn update_pi_info(conn: &Connection, data: &serde_json::Value, now: u64) -> Result<()> { let json = serde_json::to_string(data)?; conn.execute( "INSERT OR REPLACE INTO pi_info (id, data, fetched_at) VALUES (1, ?1, ?2)", rusqlite::params![json, now], )?; Ok(()) }