pub struct Store { /* private fields */ }Expand description
SQLite-based store for Aranet sensor data.
Store provides persistent storage for sensor readings, history records,
and device metadata using SQLite. It supports:
- Device management: Track multiple Aranet devices with metadata
- Current readings: Store real-time sensor data with timestamps
- History records: Cache device history to avoid re-downloading
- Incremental sync: Track sync state for efficient history updates
- Export/Import: CSV and JSON formats for data portability
§Thread Safety
Store is not thread-safe. For concurrent access (e.g., in aranet-service),
wrap it in a Mutex:
use std::sync::Arc;
use tokio::sync::Mutex;
use aranet_store::Store;
let store = Arc::new(Mutex::new(Store::open_default()?));
// In async context:
let guard = store.lock().await;
let devices = guard.list_devices()?;§Example
use aranet_store::{Store, ReadingQuery, HistoryQuery};
use aranet_types::CurrentReading;
// Open the default database
let store = Store::open_default()?;
// Store a reading
let reading = CurrentReading::default();
store.insert_reading("Aranet4 17C3C", &reading)?;
// Query readings
let query = ReadingQuery::new().device("Aranet4 17C3C").limit(10);
let readings = store.query_readings(&query)?;
// Export history to CSV
let csv = store.export_history_csv(&HistoryQuery::new())?;Implementations§
Source§impl Store
impl Store
Sourcepub fn open<P: AsRef<Path>>(path: P) -> Result<Self>
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self>
Open or create a database at the given path.
Creates parent directories if they don’t exist. The database is initialized with WAL mode for better concurrent read performance.
§Arguments
path- Path to the SQLite database file
§Example
use aranet_store::Store;
let store = Store::open("/path/to/my/aranet.db")?;Sourcepub fn open_default() -> Result<Self>
pub fn open_default() -> Result<Self>
Open the database at the platform-specific default location.
Default paths by platform:
- Linux:
~/.local/share/aranet/data.db - macOS:
~/Library/Application Support/aranet/data.db - Windows:
C:\Users\<user>\AppData\Local\aranet\data.db
§Example
use aranet_store::Store;
let store = Store::open_default()?;Sourcepub fn open_in_memory() -> Result<Self>
pub fn open_in_memory() -> Result<Self>
Open an in-memory database.
Useful for testing or temporary storage. Data is lost when the
Store is dropped.
§Example
use aranet_store::Store;
let store = Store::open_in_memory()?;
// Use for testing...Sourcepub fn database_path(&self) -> Option<&Path>
pub fn database_path(&self) -> Option<&Path>
Return the database path for file-backed stores.
In-memory stores return None.
Sourcepub fn upsert_device(
&self,
device_id: &str,
name: Option<&str>,
) -> Result<StoredDevice>
pub fn upsert_device( &self, device_id: &str, name: Option<&str>, ) -> Result<StoredDevice>
Get or create a device entry, updating timestamps.
If the device exists, updates its last_seen timestamp and optionally
the name. If it doesn’t exist, creates a new entry with the current time
as both first_seen and last_seen.
§Arguments
device_id- Unique identifier for the device (typically BLE address)name- Optional human-readable name for the device
§Returns
The device record after insert/update.
§Example
use aranet_store::Store;
let store = Store::open_in_memory()?;
let device = store.upsert_device("Aranet4 17C3C", Some("Kitchen"))?;
assert_eq!(device.name, Some("Kitchen".to_string()));Sourcepub fn update_device_metadata(
&self,
device_id: &str,
name: Option<&str>,
device_type: Option<DeviceType>,
) -> Result<()>
pub fn update_device_metadata( &self, device_id: &str, name: Option<&str>, device_type: Option<DeviceType>, ) -> Result<()>
Update device metadata (name and type).
This is a simpler version of update_device_info for when you only have
basic device information (e.g., from BLE advertisement or connection).
Sourcepub fn update_device_info(
&self,
device_id: &str,
info: &DeviceInfo,
) -> Result<()>
pub fn update_device_info( &self, device_id: &str, info: &DeviceInfo, ) -> Result<()>
Update device info from DeviceInfo.
Device type is automatically inferred from the model name using
DeviceType::from_name(), which handles all known Aranet device naming patterns.
Sourcepub fn get_device(&self, device_id: &str) -> Result<Option<StoredDevice>>
pub fn get_device(&self, device_id: &str) -> Result<Option<StoredDevice>>
Get a device by its unique identifier.
§Arguments
device_id- The device identifier to look up
§Returns
Some(StoredDevice) if found, None if the device doesn’t exist.
§Example
use aranet_store::Store;
let store = Store::open_in_memory()?;
store.upsert_device("Aranet4 17C3C", Some("Kitchen"))?;
if let Some(device) = store.get_device("Aranet4 17C3C")? {
println!("Found device: {:?}", device.name);
}Sourcepub fn list_devices(&self) -> Result<Vec<StoredDevice>>
pub fn list_devices(&self) -> Result<Vec<StoredDevice>>
List all known devices, ordered by most recently seen first.
§Returns
A vector of all stored devices, sorted by last_seen descending.
§Example
use aranet_store::Store;
let store = Store::open_in_memory()?;
store.upsert_device("device-1", Some("Kitchen"))?;
store.upsert_device("device-2", Some("Bedroom"))?;
let devices = store.list_devices()?;
for device in devices {
println!("{}: {:?}", device.id, device.name);
}Sourcepub fn delete_device(&self, device_id: &str) -> Result<bool>
pub fn delete_device(&self, device_id: &str) -> Result<bool>
Delete a device and all associated data (readings, history, sync state).
All deletions are performed within a transaction to ensure atomicity. Returns true if the device was deleted, false if it didn’t exist.
Sourcepub fn prune_history(&self, older_than: OffsetDateTime) -> Result<u64>
pub fn prune_history(&self, older_than: OffsetDateTime) -> Result<u64>
Delete history records older than the given timestamp.
Returns the number of records deleted.
Sourcepub fn prune_readings(&self, older_than: OffsetDateTime) -> Result<u64>
pub fn prune_readings(&self, older_than: OffsetDateTime) -> Result<u64>
Delete readings older than the given timestamp.
Returns the number of records deleted.
Source§impl Store
impl Store
Sourcepub fn insert_reading(
&self,
device_id: &str,
reading: &CurrentReading,
) -> Result<i64>
pub fn insert_reading( &self, device_id: &str, reading: &CurrentReading, ) -> Result<i64>
Insert a current reading from a device.
Automatically creates the device entry if it doesn’t exist. The reading
is stored with its captured_at timestamp, or the current time if not set.
§Arguments
device_id- The device that produced this readingreading- The sensor reading to store
§Returns
The database row ID of the inserted reading.
§Example
use aranet_store::Store;
use aranet_types::{CurrentReading, Status};
let store = Store::open_in_memory()?;
let reading = CurrentReading {
co2: 800,
temperature: 22.5,
pressure: 1013.0,
humidity: 45,
battery: 85,
status: Status::Green,
..Default::default()
};
let row_id = store.insert_reading("Aranet4 17C3C", &reading)?;Sourcepub fn query_readings(&self, query: &ReadingQuery) -> Result<Vec<StoredReading>>
pub fn query_readings(&self, query: &ReadingQuery) -> Result<Vec<StoredReading>>
Query readings with optional filters.
Use ReadingQuery to build queries with device, time range,
pagination, and ordering filters.
§Arguments
query- Query parameters built usingReadingQuery
§Example
use aranet_store::{Store, ReadingQuery};
use time::{OffsetDateTime, Duration};
let store = Store::open_in_memory()?;
// Query last 24 hours for a specific device
let yesterday = OffsetDateTime::now_utc() - Duration::hours(24);
let query = ReadingQuery::new()
.device("Aranet4 17C3C")
.since(yesterday)
.limit(100);
let readings = store.query_readings(&query)?;
for reading in readings {
println!("CO2: {} ppm at {}", reading.co2, reading.captured_at);
}Sourcepub fn get_latest_reading(
&self,
device_id: &str,
) -> Result<Option<StoredReading>>
pub fn get_latest_reading( &self, device_id: &str, ) -> Result<Option<StoredReading>>
Get the most recent reading for a device.
Convenience method equivalent to query_readings with limit(1).
§Arguments
device_id- The device to get the latest reading for
§Returns
The most recent reading, or None if no readings exist for this device.
§Example
use aranet_store::Store;
let store = Store::open_in_memory()?;
if let Some(reading) = store.get_latest_reading("Aranet4 17C3C")? {
println!("Latest CO2: {} ppm", reading.co2);
}Sourcepub fn list_latest_readings(&self) -> Result<Vec<(StoredDevice, StoredReading)>>
pub fn list_latest_readings(&self) -> Result<Vec<(StoredDevice, StoredReading)>>
List each device together with its latest stored reading.
Devices without readings are omitted. Results are ordered by device
last_seen descending to match Store::list_devices.
Sourcepub fn count_readings(&self, device_id: Option<&str>) -> Result<u64>
pub fn count_readings(&self, device_id: Option<&str>) -> Result<u64>
Count total readings, optionally filtered by device.
§Arguments
device_id- IfSome, count only readings for this device. IfNone, count all readings across all devices.
§Example
use aranet_store::Store;
let store = Store::open_in_memory()?;
// Count all readings
let total = store.count_readings(None)?;
// Count for specific device
let device_count = store.count_readings(Some("Aranet4 17C3C"))?;Source§impl Store
impl Store
Sourcepub fn insert_history(
&self,
device_id: &str,
records: &[HistoryRecord],
) -> Result<usize>
pub fn insert_history( &self, device_id: &str, records: &[HistoryRecord], ) -> Result<usize>
Insert history records with automatic deduplication.
Records are deduplicated by (device_id, timestamp) - if a record with
the same timestamp already exists for this device, it is skipped.
This allows safe re-syncing without creating duplicates.
§Arguments
device_id- The device these history records belong torecords- Slice of history records to insert
§Returns
The number of records actually inserted (excluding duplicates).
§Example
use aranet_store::Store;
use aranet_types::HistoryRecord;
use time::OffsetDateTime;
let store = Store::open_in_memory()?;
let records = vec![
HistoryRecord {
timestamp: OffsetDateTime::now_utc(),
co2: 800,
temperature: 22.5,
pressure: 1013.0,
humidity: 45,
radon: None,
radiation_rate: None,
radiation_total: None,
},
];
let inserted = store.insert_history("Aranet4 17C3C", &records)?;
println!("Inserted {} new records", inserted);Sourcepub fn query_history(
&self,
query: &HistoryQuery,
) -> Result<Vec<StoredHistoryRecord>>
pub fn query_history( &self, query: &HistoryQuery, ) -> Result<Vec<StoredHistoryRecord>>
Query history records with optional filters.
Use HistoryQuery to build queries with device, time range,
pagination, and ordering filters.
§Arguments
query- Query parameters built usingHistoryQuery
§Example
use aranet_store::{Store, HistoryQuery};
use time::{OffsetDateTime, Duration};
let store = Store::open_in_memory()?;
// Query last week's history for a device
let week_ago = OffsetDateTime::now_utc() - Duration::days(7);
let query = HistoryQuery::new()
.device("Aranet4 17C3C")
.since(week_ago)
.oldest_first();
let records = store.query_history(&query)?;Sourcepub fn count_history(&self, device_id: Option<&str>) -> Result<u64>
pub fn count_history(&self, device_id: Option<&str>) -> Result<u64>
Count total history records, optionally filtered by device.
§Arguments
device_id- IfSome, count only records for this device. IfNone, count all records across all devices.
§Example
use aranet_store::Store;
let store = Store::open_in_memory()?;
// Count all history records
let total = store.count_history(None)?;
// Count for specific device
let device_count = store.count_history(Some("Aranet4 17C3C"))?;Source§impl Store
impl Store
Sourcepub fn get_sync_state(&self, device_id: &str) -> Result<Option<SyncState>>
pub fn get_sync_state(&self, device_id: &str) -> Result<Option<SyncState>>
Get the sync state for a device.
Sync state tracks the last downloaded history index and total readings, enabling incremental history downloads instead of re-downloading everything.
§Arguments
device_id- The device to get sync state for
§Returns
The sync state if any history has been synced, None for new devices.
§Example
use aranet_store::Store;
let store = Store::open_in_memory()?;
store.upsert_device("Aranet4 17C3C", None)?;
// Initially no sync state
let state = store.get_sync_state("Aranet4 17C3C")?;
assert!(state.is_none());Sourcepub fn update_sync_state(
&self,
device_id: &str,
last_index: u16,
total_readings: u16,
) -> Result<()>
pub fn update_sync_state( &self, device_id: &str, last_index: u16, total_readings: u16, ) -> Result<()>
Update sync state after a successful history download.
Call this after downloading history records to track progress. The next
sync can then use calculate_sync_start to
determine which records to download.
§Arguments
device_id- The device that was syncedlast_index- The highest history index that was downloaded (1-based)total_readings- Total readings on the device at sync time
§Example
use aranet_store::Store;
let store = Store::open_in_memory()?;
store.upsert_device("Aranet4 17C3C", None)?;
// After downloading all 500 history records
store.update_sync_state("Aranet4 17C3C", 500, 500)?;
// Verify sync state was saved
let state = store.get_sync_state("Aranet4 17C3C")?.unwrap();
assert_eq!(state.last_history_index, Some(500));Sourcepub fn calculate_sync_start(
&self,
device_id: &str,
current_total: u16,
) -> Result<u16>
pub fn calculate_sync_start( &self, device_id: &str, current_total: u16, ) -> Result<u16>
Calculate the start index for incremental sync.
Returns the index to start downloading from (1-based). If the device has new readings since last sync, returns the next index. If this is the first sync, returns 1 to download all.
§Buffer Wrap-Around Detection
Aranet devices have a circular buffer (e.g., ~2016 readings for Aranet4 at 10-min
intervals). When the buffer fills up, new readings replace the oldest ones, but
total_readings stays constant. This function detects this wrap-around case by
comparing the latest stored timestamp with the expected time since last sync.
Source§impl Store
impl Store
Sourcepub fn history_stats(&self, query: &HistoryQuery) -> Result<HistoryStats>
pub fn history_stats(&self, query: &HistoryQuery) -> Result<HistoryStats>
Calculate aggregate statistics for history records.
Computes min, max, and average values for all sensor metrics across the records matching the query. Useful for dashboards and reports.
§Arguments
query- Filter which records to include in the statistics
§Example
use aranet_store::{Store, HistoryQuery};
use time::{OffsetDateTime, Duration};
let store = Store::open_in_memory()?;
// Get stats for last 24 hours
let yesterday = OffsetDateTime::now_utc() - Duration::hours(24);
let query = HistoryQuery::new()
.device("Aranet4 17C3C")
.since(yesterday);
let stats = store.history_stats(&query)?;
if let Some(avg_co2) = stats.avg.co2 {
println!("Average CO2: {:.0} ppm", avg_co2);
}
if let Some((start, end)) = stats.time_range {
println!("Time range: {} to {}", start, end);
}Sourcepub fn export_history_csv(&self, query: &HistoryQuery) -> Result<String>
pub fn export_history_csv(&self, query: &HistoryQuery) -> Result<String>
Export history records to CSV format.
Exports records matching the query to a CSV string with the following columns:
timestamp, device_id, co2, temperature, pressure, humidity, radon.
Timestamps are formatted as RFC 3339 (e.g., 2024-01-15T10:30:00Z).
§Arguments
query- Filter which records to export
§Example
use aranet_store::{Store, HistoryQuery};
use std::fs;
let store = Store::open_in_memory()?;
let query = HistoryQuery::new().device("Aranet4 17C3C").oldest_first();
let csv = store.export_history_csv(&query)?;
// Write to file
// fs::write("history.csv", &csv)?;Sourcepub fn export_history_json(&self, query: &HistoryQuery) -> Result<String>
pub fn export_history_json(&self, query: &HistoryQuery) -> Result<String>
Export history records to JSON format.
Exports records matching the query as a pretty-printed JSON array of
StoredHistoryRecord objects.
§Arguments
query- Filter which records to export
§Example
use aranet_store::{Store, HistoryQuery};
let store = Store::open_in_memory()?;
let query = HistoryQuery::new().device("Aranet4 17C3C");
let json = store.export_history_json(&query)?;
println!("{}", json);Sourcepub fn import_history_csv(&self, csv_data: &str) -> Result<ImportResult>
pub fn import_history_csv(&self, csv_data: &str) -> Result<ImportResult>
Import history records from CSV format.
Expected CSV format:
timestamp,device_id,co2,temperature,pressure,humidity,radon
2024-01-15T10:30:00Z,Aranet4 17C3C,800,22.5,1013.25,45,Returns the number of records imported (deduplicated by device_id + timestamp).
Sourcepub fn import_history_json(&self, json_data: &str) -> Result<ImportResult>
pub fn import_history_json(&self, json_data: &str) -> Result<ImportResult>
Import history records from JSON format.
Expected JSON format: an array of StoredHistoryRecord objects.
Returns the number of records imported (deduplicated by device_id + timestamp).