131 lines
4 KiB
Rust
131 lines
4 KiB
Rust
use anyhow::{Context, Result};
|
|
use rusqlite::Connection;
|
|
|
|
use crate::types::{DoorStatus, HistoryEntry, StatusResponse};
|
|
|
|
pub fn init(path: &str) -> Result<Connection> {
|
|
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<StatusResponse> {
|
|
let (status, timestamp, last_seen) = conn.query_row(
|
|
"SELECT status, timestamp, last_seen FROM current_state WHERE id = 1",
|
|
[],
|
|
|row| {
|
|
Ok((
|
|
row.get::<_, Option<String>>(0)?,
|
|
row.get::<_, Option<u64>>(1)?,
|
|
row.get::<_, Option<u64>>(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<Option<String>> {
|
|
let status = conn.query_row(
|
|
"SELECT status FROM current_state WHERE id = 1",
|
|
[],
|
|
|row| row.get::<_, Option<String>>(0),
|
|
)?;
|
|
Ok(status)
|
|
}
|
|
|
|
pub fn get_history(conn: &Connection, limit: u32) -> Result<Vec<HistoryEntry>> {
|
|
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::<Result<Vec<_>, _>>()?;
|
|
Ok(entries)
|
|
}
|
|
|
|
pub fn get_pi_info(conn: &Connection) -> Result<serde_json::Value> {
|
|
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(())
|
|
}
|