impl Db { pub fn open(path: &Path) -> Result { let conn = Connection::open(path) .with_context(|| format!("cannot open SQLite database {}", path.display()))?; conn.busy_timeout(std::time::Duration::from_secs(5))?; conn.execute_batch(queries::INIT_SCHEMA)?; Ok(Self { conn: Arc::new(Mutex::new(conn)), }) } fn lock(&self) -> Result> { self.conn .lock() .map_err(|_| anyhow::anyhow!("database mutex poisoned")) } fn from_json(payload: String) -> Result { Ok(serde_json::from_str(&payload)?) } fn to_json(value: &T) -> Result { Ok(serde_json::to_string(value)?) } /// Device protocol keys are intentionally omitted by the public `Device` serializer. /// Persist them only in the private SQLite payload so normal API responses never expose them. fn device_to_storage_json(device: &Device) -> Result { let mut value = serde_json::to_value(device)?; if let Some(object) = value.as_object_mut() { if let Some(key) = device.key.as_ref().filter(|key| !key.is_empty()) { object.insert("key".into(), serde_json::Value::String(key.clone())); } } Ok(serde_json::to_string(&value)?) } pub fn count_devices(&self) -> Result { let conn = self.lock()?; let count: i64 = conn.query_row(queries::COUNT_DEVICES, [], |row| row.get(0))?; Ok(count.max(0) as u64) } pub fn save_device(&self, device: &Device) -> Result<()> { let payload = Self::device_to_storage_json(device)?; let index_mac = match device.connection_type { ConnectionType::Local => device.mac.clone(), ConnectionType::GreeCloud => format!("cloud:{}", device.cloud_device_id.as_deref().unwrap_or(&device.mac)), }; let conn = self.lock()?; conn.execute( queries::UPSERT_DEVICE, params![ device.id, index_mac, device.name, device.ip, device.simulated as i64, payload, device.updated_at.to_rfc3339() ], )?; Ok(()) } pub fn list_devices(&self) -> Result> { let conn = self.lock()?; let mut stmt = conn.prepare(queries::LIST_DEVICES)?; let payloads = stmt .query_map([], |row| row.get::<_, String>(0))? .collect::, _>>()?; payloads.into_iter().map(Self::from_json).collect() } pub fn get_device(&self, id: &str) -> Result> { let conn = self.lock()?; let payload: Option = conn .query_row(queries::GET_DEVICE_BY_ID, [id], |row| row.get(0)) .optional()?; payload.map(Self::from_json).transpose() } pub fn get_device_by_mac(&self, mac: &str) -> Result> { let conn = self.lock()?; let payload: Option = conn .query_row(queries::GET_DEVICE_BY_MAC, [mac], |row| row.get(0)) .optional()?; payload.map(Self::from_json).transpose() } pub fn delete_device(&self, id: &str) -> Result { let mut conn = self.lock()?; let tx = conn.transaction()?; tx.execute(queries::DELETE_DEVICE_READINGS, [id])?; tx.execute(queries::DELETE_DEVICE_ENERGY_READINGS, [id])?; tx.execute(queries::DELETE_ZONE_READINGS_BY_DEVICE_ID, [id])?; tx.execute(queries::DELETE_SCHEDULES_BY_DEVICE_ID, [id])?; tx.execute(queries::DELETE_ZONES_BY_DEVICE_ID, [id])?; let changed = tx.execute(queries::DELETE_DEVICE, [id])? > 0; tx.commit()?; Ok(changed) } }