use crate::data::IoTData;
use crate::{Error, Result};
use ones_oidc::{OpenIdconnectClient, get_well_known_application_by_client_identifier};
use openidconnect::OAuth2TokenResponse;
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IotDeviceExistsResponse {
pub exists: bool,
#[serde(default)]
pub remember_device: bool,
pub id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IotDeviceCreateResponse {
pub hardware_identifier: String,
pub model: String,
pub connection_type: String,
pub connection_meta: HashMap<String, serde_json::Value>,
pub supported_data_types: Vec<String>,
pub id: String,
pub created_at: String,
pub modified_at: String,
pub revision: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiSubmissionPayload {
pub data: Vec<ApiDeviceDataItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiDeviceDataItem {
pub iot_device_id: String,
pub data_type: String,
pub data: Vec<ApiDataPoint>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiDataPoint {
pub timestamp: i64,
pub data: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct RegisteredDeviceInfo {
pub address: String,
pub backend_device_id: Option<String>,
pub approved: bool,
}
pub struct BackendAPI {
client: reqwest::Client,
auth_host: Option<String>,
app_host: Option<String>,
identifier: Option<String>,
timeout: Duration,
oidc_client: Option<Arc<OpenIdconnectClient>>,
mock_token: Option<String>,
}
impl BackendAPI {
pub fn new(
identifier: Option<String>,
timeout: Duration,
oidc_client: Option<Arc<OpenIdconnectClient>>,
mock_backend_url: Option<String>,
mock_bearer_token: Option<String>,
) -> Self {
let client = reqwest::Client::builder()
.timeout(timeout)
.build()
.expect("Failed to create HTTP client");
Self {
client,
auth_host: None,
app_host: mock_backend_url,
identifier,
timeout,
oidc_client,
mock_token: mock_bearer_token,
}
}
pub fn is_mock_mode(&self) -> bool {
self.mock_token.is_some()
}
pub async fn ready_api(&mut self) -> Result<()> {
if self.is_mock_mode() {
if self.app_host.is_none() {
return Err(Error::Config("Mock mode requires mock_backend_url to be set".to_string()));
}
return Ok(());
}
if self.identifier.is_none() {
return Err(Error::Config("Well known identifier is not set".to_string()));
}
let (_access_token, app_host) = self.access_token_and_host().await?;
self.app_host = Some(app_host);
Ok(())
}
pub async fn access_token_and_host(&self) -> Result<(String, String)> {
let identifier = self.identifier.as_ref()
.ok_or_else(|| Error::Config("Identifier not set".to_string()))?;
let oidc_client = self.oidc_client.as_ref()
.ok_or_else(|| Error::Config("OIDC client not initialized".to_string()))?;
let token_response = oidc_client.device_access_token().await
.map_err(|e| Error::Api(format!("Failed to get device access token: {}", e)))?;
let access_token = token_response.access_token().secret();
let app_info = get_well_known_application_by_client_identifier(
oidc_client.provider_metadata(),
&access_token,
identifier
).await
.map_err(|e| Error::Api(format!("Failed to get application info: {}", e)))?;
let app_host = app_info.url(oidc_client.issuer_url().as_str());
Ok((access_token.to_string(), app_host))
}
pub async fn access_token(&self) -> Result<String> {
if let Some(ref token) = self.mock_token {
return Ok(token.clone());
}
let (access_token, _) = self.access_token_and_host().await?;
Ok(access_token)
}
fn make_bearer_headers(&self, access_token: &str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", access_token))
.expect("Invalid authorization header"),
);
headers.insert(
CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
headers
}
pub async fn submit_iot_data_formatted(&mut self, payload: ApiSubmissionPayload) -> Result<()> {
self.ready_api().await?;
let app_host = self.app_host.as_ref()
.ok_or_else(|| Error::Api("App host not set".to_string()))?;
let access_token = self.access_token().await?;
let url = format!("{}/api/iot-devices/data/submit", app_host);
let response = self.client
.post(&url)
.headers(self.make_bearer_headers(&access_token))
.json(&payload)
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await?;
return Err(Error::Api(format!("HTTP {}: {}", status, body)));
}
Ok(())
}
pub async fn submit_iot_data(&mut self, data: Vec<IoTData>) -> Result<()> {
let payload = Self::dump_iot_data_for_api_submission(data, &[]);
self.submit_iot_data_formatted(payload).await
}
pub async fn submit_iot_data_sync(&mut self, data: Vec<IoTData>) -> Result<()> {
self.submit_iot_data(data).await
}
pub async fn iot_device_exists(&mut self, address: &str) -> Result<IotDeviceExistsResponse> {
self.ready_api().await?;
let app_host = self.app_host.as_ref()
.ok_or_else(|| Error::Api("App host not set".to_string()))?;
let access_token = self.access_token().await?;
let url = format!("{}/api/iot-devices/exists", app_host);
let payload = serde_json::json!({
"hardwareIdentifier": address
});
let response = self.client
.post(&url)
.headers(self.make_bearer_headers(&access_token))
.json(&payload)
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await?;
return Err(Error::Api(format!("HTTP {}: {}", status, body)));
}
let data: serde_json::Value = response.json().await?;
let exists = data.get("exists")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let remember_device = data.get("rememberDevice")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let id = if exists && remember_device {
data.get("id")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
} else {
None
};
Ok(IotDeviceExistsResponse {
exists,
remember_device,
id,
})
}
pub async fn create_iot_device(
&mut self,
hardware_identifier: &str,
model: Option<&str>,
connection_type: Option<&str>,
connection_meta: Option<HashMap<String, serde_json::Value>>,
supported_data_types: Option<Vec<String>>,
) -> Result<IotDeviceCreateResponse> {
self.ready_api().await?;
let app_host = self.app_host.as_ref()
.ok_or_else(|| Error::Api("App host not set".to_string()))?;
let access_token = self.access_token().await?;
let url = format!("{}/api/iot-devices", app_host);
let payload = serde_json::json!({
"hardwareIdentifier": hardware_identifier,
"model": model.unwrap_or("generic"),
"connectionType": connection_type.unwrap_or("bluetooth_le"),
"connectionMeta": connection_meta.unwrap_or_default(),
"supportedDataTypes": supported_data_types.unwrap_or_default(),
});
let response = self.client
.post(&url)
.headers(self.make_bearer_headers(&access_token))
.json(&payload)
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await?;
return Err(Error::Api(format!("HTTP {}: {}", status, body)));
}
let device: IotDeviceCreateResponse = response.json().await?;
Ok(device)
}
pub async fn create_iot_device_if_not_exists(
&mut self,
address: &str,
model: Option<&str>,
connection_type: Option<&str>,
connection_meta: Option<HashMap<String, serde_json::Value>>,
supported_data_types: Option<Vec<String>>,
) -> Result<serde_json::Value> {
let exists = self.iot_device_exists(address).await?;
if exists.exists {
Ok(serde_json::json!({
"exists": exists.exists,
"remember_device": exists.remember_device,
"id": exists.id
}))
} else {
let device = self.create_iot_device(address, model, connection_type, connection_meta, supported_data_types).await?;
Ok(serde_json::to_value(device)?)
}
}
pub fn dump_iot_data_for_api_submission(
input_data: Vec<IoTData>,
registered_devices: &[RegisteredDeviceInfo],
) -> ApiSubmissionPayload {
use tracing::{debug, warn};
let mut device_id_map: HashMap<String, String> = HashMap::new();
for device in registered_devices {
if device.approved {
if let Some(ref backend_id) = device.backend_device_id {
device_id_map.insert(device.address.clone(), backend_id.clone());
}
}
}
let mut data_by_device: HashMap<String, Vec<IoTData>> = HashMap::new();
for data in input_data {
if device_id_map.contains_key(data.address()) {
data_by_device
.entry(data.address().to_string())
.or_insert_with(Vec::new)
.push(data);
} else {
debug!("Skipping unregistered device {}", data.address());
}
}
let mut api_data_items = Vec::new();
for (address, device_data) in data_by_device {
if let Some(backend_device_id) = device_id_map.get(&address) {
let mut data_by_type: HashMap<String, Vec<ApiDataPoint>> = HashMap::new();
for data in device_data {
let data_type = data.name().to_string();
let api_point = ApiDataPoint {
timestamp: data.timestamp(),
data: match &data {
IoTData::BatteryLevel(d) => {
let mut m = HashMap::new();
m.insert("level".to_string(), serde_json::json!(d.level));
m
},
IoTData::HeartRate(d) => {
let mut m = HashMap::new();
m.insert("rate".to_string(), serde_json::json!(d.rate));
m
},
IoTData::Temperature(d) => {
let mut m = HashMap::new();
m.insert("level".to_string(), serde_json::json!(d.level));
m
},
IoTData::Pressure(d) => {
let mut m = HashMap::new();
m.insert("level".to_string(), serde_json::json!(d.level));
m
},
IoTData::BloodPressure(d) => {
let mut m = HashMap::new();
m.insert("sys".to_string(), serde_json::json!(d.sys));
m.insert("dias".to_string(), serde_json::json!(d.dias));
m
},
IoTData::Humidity(d) => {
let mut m = HashMap::new();
m.insert("level".to_string(), serde_json::json!(d.level));
m
},
IoTData::Alert(d) => {
let mut m = HashMap::new();
m.insert("id".to_string(), serde_json::json!(d.id));
m
},
IoTData::Generic(d) => d.data.clone(),
},
};
data_by_type
.entry(data_type)
.or_insert_with(Vec::new)
.push(api_point);
}
for (data_type, data_points) in data_by_type {
api_data_items.push(ApiDeviceDataItem {
iot_device_id: backend_device_id.clone(),
data_type,
data: data_points,
});
}
}
}
ApiSubmissionPayload {
data: api_data_items,
}
}
pub async fn last_iot_device_data_submission(&mut self, iot_device_id: &str) -> Result<i64> {
self.ready_api().await?;
let app_host = self.app_host.as_ref()
.ok_or_else(|| Error::Api("App host not set".to_string()))?;
let access_token = self.access_token().await?;
let url = format!("{}/api/iot-devices/{}/last-data-submission", app_host, iot_device_id);
let response = self.client
.get(&url)
.headers(self.make_bearer_headers(&access_token))
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await?;
return Err(Error::Api(format!("HTTP {}: {}", status, body)));
}
let data: serde_json::Value = response.json().await?;
let timestamp = data.get("timestamp")
.and_then(|v| v.as_i64())
.unwrap_or(0);
Ok(timestamp)
}
}