package datastore import ( "database/sql" "fmt" "iter" "time" _ "github.com/mattn/go-sqlite3" ) // DataStore handles all database operations type DataStore struct { db *sql.DB } // New creates a new DataStore instance func New(dbPath string) (*DataStore, error) { db, err := sql.Open("sqlite3", dbPath) if err != nil { return nil, err } ds := &DataStore{db: db} if err := ds.initTables(); err != nil { db.Close() return nil, err } return ds, nil } // Close closes the database connection func (ds *DataStore) Close() error { return ds.db.Close() } // initTables creates the necessary database tables func (ds *DataStore) initTables() error { tables := []string{ `CREATE TABLE IF NOT EXISTS devices ( id INTEGER PRIMARY KEY, name TEXT, unique_id TEXT UNIQUE )`, `CREATE TABLE IF NOT EXISTS sensors ( sensor_id INTEGER PRIMARY KEY AUTOINCREMENT, protocol TEXT, model TEXT, id INTEGER, name TEXT, temperature_unique_id TEXT, humidity_unique_id TEXT, last_temperature TEXT, last_humidity TEXT, last_timestamp INTEGER, hidden INTEGER DEFAULT 0, UNIQUE(protocol, model, id) )`, `CREATE TABLE IF NOT EXISTS potential_devices ( id INTEGER PRIMARY KEY AUTOINCREMENT, class TEXT, protocol TEXT, model TEXT, device_id TEXT, last_data TEXT, last_seen INTEGER )`, } for _, table := range tables { if _, err := ds.db.Exec(table); err != nil { return err } } return nil } // UpsertDevice inserts or updates a device func (ds *DataStore) UpsertDevice(device *Device) error { _, err := ds.db.Exec( "INSERT OR REPLACE INTO devices (id, name, unique_id) VALUES (?, ?, ?)", device.ID, device.Name, device.UniqueID, ) return err } // GetDevice retrieves a device by ID func (ds *DataStore) GetDevice(id int) (*Device, error) { device := &Device{} err := ds.db.QueryRow( "SELECT id, name, unique_id FROM devices WHERE id = ?", id, ).Scan(&device.ID, &device.Name, &device.UniqueID) if err != nil { return nil, err } return device, nil } // ListDevices returns an iterator over all devices func (ds *DataStore) ListDevices() iter.Seq[*Device] { return func(yield func(*Device) bool) { rows, err := ds.db.Query("SELECT id, name, unique_id FROM devices ORDER BY id") if err != nil { return } defer rows.Close() for rows.Next() { device := &Device{} if err := rows.Scan(&device.ID, &device.Name, &device.UniqueID); err != nil { continue } if !yield(device) { return } } } } // UpdateDeviceName updates a device's name func (ds *DataStore) UpdateDeviceName(id int, name string) error { _, err := ds.db.Exec("UPDATE devices SET name = ? WHERE id = ?", name, id) return err } // UpsertSensor inserts or updates a sensor func (ds *DataStore) UpsertSensor(sensor *Sensor) error { _, err := ds.db.Exec( `INSERT OR IGNORE INTO sensors (protocol, model, id, name, temperature_unique_id, humidity_unique_id) VALUES (?, ?, ?, ?, ?, ?)`, sensor.Protocol, sensor.Model, sensor.ID, sensor.Name, sensor.TemperatureUniqueID, sensor.HumidityUniqueID, ) return err } // GetSensor retrieves a sensor by sensor_id func (ds *DataStore) GetSensor(sensorID int) (*Sensor, error) { sensor := &Sensor{} var lastTemp, lastHum sql.NullString var lastTs sql.NullInt64 var hidden int err := ds.db.QueryRow( `SELECT sensor_id, protocol, model, id, name, temperature_unique_id, humidity_unique_id, last_temperature, last_humidity, last_timestamp, hidden FROM sensors WHERE sensor_id = ?`, sensorID, ).Scan( &sensor.SensorID, &sensor.Protocol, &sensor.Model, &sensor.ID, &sensor.Name, &sensor.TemperatureUniqueID, &sensor.HumidityUniqueID, &lastTemp, &lastHum, &lastTs, &hidden, ) if err != nil { return nil, err } sensor.LastTemperature = lastTemp.String sensor.LastHumidity = lastHum.String sensor.LastTimestamp = lastTs.Int64 sensor.Hidden = hidden != 0 return sensor, nil } // GetSensorByIdentity retrieves a sensor by protocol, model, and id func (ds *DataStore) GetSensorByIdentity(protocol, model string, id int) (*Sensor, error) { sensor := &Sensor{} var lastTemp, lastHum sql.NullString var lastTs sql.NullInt64 var hidden int err := ds.db.QueryRow( `SELECT sensor_id, protocol, model, id, name, temperature_unique_id, humidity_unique_id, last_temperature, last_humidity, last_timestamp, hidden FROM sensors WHERE protocol = ? AND model = ? AND id = ?`, protocol, model, id, ).Scan( &sensor.SensorID, &sensor.Protocol, &sensor.Model, &sensor.ID, &sensor.Name, &sensor.TemperatureUniqueID, &sensor.HumidityUniqueID, &lastTemp, &lastHum, &lastTs, &hidden, ) if err != nil { return nil, err } sensor.LastTemperature = lastTemp.String sensor.LastHumidity = lastHum.String sensor.LastTimestamp = lastTs.Int64 sensor.Hidden = hidden != 0 return sensor, nil } // ListSensors returns an iterator over all sensors func (ds *DataStore) ListSensors() iter.Seq[*Sensor] { return func(yield func(*Sensor) bool) { rows, err := ds.db.Query( `SELECT sensor_id, protocol, model, id, name, temperature_unique_id, humidity_unique_id, last_temperature, last_humidity, last_timestamp, hidden FROM sensors ORDER BY sensor_id`, ) if err != nil { return } defer rows.Close() for rows.Next() { sensor := &Sensor{} var lastTemp, lastHum sql.NullString var lastTs sql.NullInt64 var hidden int if err := rows.Scan( &sensor.SensorID, &sensor.Protocol, &sensor.Model, &sensor.ID, &sensor.Name, &sensor.TemperatureUniqueID, &sensor.HumidityUniqueID, &lastTemp, &lastHum, &lastTs, &hidden, ); err != nil { continue } sensor.LastTemperature = lastTemp.String sensor.LastHumidity = lastHum.String sensor.LastTimestamp = lastTs.Int64 sensor.Hidden = hidden != 0 if !yield(sensor) { return } } } } // UpdateSensorName updates a sensor's name func (ds *DataStore) UpdateSensorName(sensorID int, name string) error { _, err := ds.db.Exec("UPDATE sensors SET name = ? WHERE sensor_id = ?", name, sensorID) return err } // UpdateSensorValue updates a sensor's last value func (ds *DataStore) UpdateSensorValue(protocol, model string, id int, dataType int, value string) error { column := "" switch dataType { case 1: // Temperature column = "last_temperature" case 2: // Humidity column = "last_humidity" default: return fmt.Errorf("unsupported data type: %d", dataType) } query := fmt.Sprintf( "UPDATE sensors SET %s = ?, last_timestamp = ? WHERE protocol = ? AND model = ? AND id = ?", column, ) _, err := ds.db.Exec(query, value, time.Now().Unix(), protocol, model, id) return err } // SetSensorHidden updates a sensor's hidden status func (ds *DataStore) SetSensorHidden(sensorID int, hidden bool) error { hiddenVal := 0 if hidden { hiddenVal = 1 } _, err := ds.db.Exec("UPDATE sensors SET hidden = ? WHERE sensor_id = ?", hiddenVal, sensorID) return err } // UpsertPotentialDevice inserts or updates a potential device func (ds *DataStore) UpsertPotentialDevice(device *PotentialDevice) error { _, err := ds.db.Exec( `INSERT OR REPLACE INTO potential_devices (class, protocol, model, device_id, last_data, last_seen) VALUES (?, ?, ?, ?, ?, ?)`, device.Class, device.Protocol, device.Model, device.DeviceID, device.LastData, device.LastSeen, ) return err } // ListPotentialDevices returns an iterator over all potential devices func (ds *DataStore) ListPotentialDevices() iter.Seq[*PotentialDevice] { return func(yield func(*PotentialDevice) bool) { rows, err := ds.db.Query( `SELECT id, class, protocol, model, device_id, last_data, last_seen FROM potential_devices ORDER BY last_seen DESC`, ) if err != nil { return } defer rows.Close() for rows.Next() { device := &PotentialDevice{} if err := rows.Scan( &device.ID, &device.Class, &device.Protocol, &device.Model, &device.DeviceID, &device.LastData, &device.LastSeen, ); err != nil { continue } if !yield(device) { return } } } }