Skip to main content

Store

Struct Store 

Source
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

Source

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")?;
Source

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()?;
Source

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...
Source

pub fn database_path(&self) -> Option<&Path>

Return the database path for file-backed stores.

In-memory stores return None.

Source

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()));
Source

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).

Source

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.

Source

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);
}
Source

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);
}
Source

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.

Source

pub fn prune_history(&self, older_than: OffsetDateTime) -> Result<u64>

Delete history records older than the given timestamp.

Returns the number of records deleted.

Source

pub fn prune_readings(&self, older_than: OffsetDateTime) -> Result<u64>

Delete readings older than the given timestamp.

Returns the number of records deleted.

Source

pub fn vacuum(&self) -> Result<()>

Reclaim unused disk space after deletions.

Source§

impl Store

Source

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 reading
  • reading - 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)?;
Source

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
§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);
}
Source

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);
}
Source

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.

Source

pub fn count_readings(&self, device_id: Option<&str>) -> Result<u64>

Count total readings, optionally filtered by device.

§Arguments
  • device_id - If Some, count only readings for this device. If None, 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

Source

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 to
  • records - 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);
Source

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
§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)?;
Source

pub fn count_history(&self, device_id: Option<&str>) -> Result<u64>

Count total history records, optionally filtered by device.

§Arguments
  • device_id - If Some, count only records for this device. If None, 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

Source

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());
Source

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 synced
  • last_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));
Source

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

Source

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);
}
Source

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)?;
Source

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);
Source

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).

Source

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).

Auto Trait Implementations§

§

impl !Freeze for Store

§

impl !RefUnwindSafe for Store

§

impl Send for Store

§

impl !Sync for Store

§

impl Unpin for Store

§

impl UnsafeUnpin for Store

§

impl !UnwindSafe for Store

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more