koprogo_api/application/dto/
iot_dto.rs

1use crate::domain::entities::{DeviceType, IoTReading, LinkyDevice, LinkyProvider, MetricType};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6// ========================================
7// IoT Reading DTOs
8// ========================================
9
10/// DTO for creating a new IoT reading
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CreateIoTReadingDto {
13    pub building_id: Uuid,
14    pub device_type: DeviceType,
15    pub metric_type: MetricType,
16    pub value: f64,
17    pub unit: String,
18    pub timestamp: DateTime<Utc>,
19    pub source: String,
20    pub metadata: Option<serde_json::Value>,
21}
22
23/// DTO for IoT reading response
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct IoTReadingResponseDto {
26    pub id: Uuid,
27    pub building_id: Uuid,
28    pub device_type: DeviceType,
29    pub metric_type: MetricType,
30    pub value: f64,
31    pub normalized_value: f64, // Value converted to standard unit
32    pub unit: String,
33    pub timestamp: DateTime<Utc>,
34    pub source: String,
35    pub metadata: Option<serde_json::Value>,
36    pub created_at: DateTime<Utc>,
37}
38
39impl From<IoTReading> for IoTReadingResponseDto {
40    fn from(reading: IoTReading) -> Self {
41        Self {
42            id: reading.id,
43            building_id: reading.building_id,
44            device_type: reading.device_type,
45            metric_type: reading.metric_type,
46            normalized_value: reading.normalized_value(),
47            value: reading.value,
48            unit: reading.unit,
49            timestamp: reading.timestamp,
50            source: reading.source,
51            metadata: reading.metadata,
52            created_at: reading.created_at,
53        }
54    }
55}
56
57/// DTO for consumption statistics
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ConsumptionStatsDto {
60    pub building_id: Uuid,
61    pub metric_type: MetricType,
62    pub period_start: DateTime<Utc>,
63    pub period_end: DateTime<Utc>,
64    pub total_consumption: f64,
65    pub average_daily: f64,
66    pub min_value: f64,
67    pub max_value: f64,
68    pub reading_count: i64,
69    pub unit: String,
70    pub source: String,
71}
72
73/// DTO for anomaly detection results
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct AnomalyDetectionDto {
76    pub reading: IoTReadingResponseDto,
77    pub is_anomalous: bool,
78    pub average_value: f64,
79    pub deviation_percentage: f64,
80    pub threshold_percentage: f64,
81}
82
83// ========================================
84// Linky Device DTOs
85// ========================================
86
87/// DTO for configuring a new Linky device
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct ConfigureLinkyDeviceDto {
90    pub building_id: Uuid,
91    pub prm: String,
92    pub provider: LinkyProvider,
93    pub authorization_code: String, // OAuth2 authorization code (will be exchanged for tokens)
94}
95
96/// DTO for Linky device response
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct LinkyDeviceResponseDto {
99    pub id: Uuid,
100    pub building_id: Uuid,
101    pub prm: String,
102    pub provider: LinkyProvider,
103    pub last_sync_at: Option<DateTime<Utc>>,
104    pub sync_enabled: bool,
105    pub token_expires_at: Option<DateTime<Utc>>,
106    pub is_token_expired: bool,
107    pub needs_sync: bool,
108    pub api_endpoint: String,
109    pub created_at: DateTime<Utc>,
110    pub updated_at: DateTime<Utc>,
111}
112
113impl From<LinkyDevice> for LinkyDeviceResponseDto {
114    fn from(device: LinkyDevice) -> Self {
115        Self {
116            id: device.id,
117            building_id: device.building_id,
118            prm: device.prm.clone(),
119            provider: device.provider,
120            last_sync_at: device.last_sync_at,
121            sync_enabled: device.sync_enabled,
122            token_expires_at: device.token_expires_at,
123            is_token_expired: device.is_token_expired(),
124            needs_sync: device.needs_sync(),
125            api_endpoint: device.api_endpoint().to_string(),
126            created_at: device.created_at,
127            updated_at: device.updated_at,
128        }
129    }
130}
131
132/// DTO for triggering Linky data sync
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct SyncLinkyDataDto {
135    pub building_id: Uuid,
136    pub start_date: DateTime<Utc>,
137    pub end_date: DateTime<Utc>,
138}
139
140/// DTO for Linky sync response
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct LinkySyncResponseDto {
143    pub device_id: Uuid,
144    pub sync_started_at: DateTime<Utc>,
145    pub start_date: DateTime<Utc>,
146    pub end_date: DateTime<Utc>,
147    pub readings_fetched: usize,
148    pub success: bool,
149    pub error_message: Option<String>,
150}
151
152// ========================================
153// Query DTOs
154// ========================================
155
156/// DTO for querying IoT readings
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct QueryIoTReadingsDto {
159    pub building_id: Uuid,
160    pub device_type: Option<DeviceType>,
161    pub metric_type: Option<MetricType>,
162    pub start_date: DateTime<Utc>,
163    pub end_date: DateTime<Utc>,
164    pub limit: Option<usize>,
165}
166
167impl Default for QueryIoTReadingsDto {
168    fn default() -> Self {
169        let now = Utc::now();
170        Self {
171            building_id: Uuid::nil(), // Must be set by caller
172            device_type: None,
173            metric_type: None,
174            start_date: now - chrono::Duration::days(30),
175            end_date: now,
176            limit: Some(1000),
177        }
178    }
179}
180
181/// DTO for daily aggregated readings
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct DailyAggregateDto {
184    pub building_id: Uuid,
185    pub device_type: DeviceType,
186    pub metric_type: MetricType,
187    pub day: DateTime<Utc>,
188    pub avg_value: f64,
189    pub min_value: f64,
190    pub max_value: f64,
191    pub total_value: f64,
192    pub reading_count: i64,
193    pub source: String,
194}
195
196/// DTO for monthly aggregated readings
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct MonthlyAggregateDto {
199    pub building_id: Uuid,
200    pub device_type: DeviceType,
201    pub metric_type: MetricType,
202    pub month: DateTime<Utc>,
203    pub avg_value: f64,
204    pub min_value: f64,
205    pub max_value: f64,
206    pub total_value: f64,
207    pub reading_count: i64,
208    pub source: String,
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_iot_reading_response_dto_from_entity() {
217        let reading = IoTReading::new(
218            Uuid::new_v4(),
219            DeviceType::ElectricityMeter,
220            MetricType::ElectricityConsumption,
221            150.5,
222            "kWh".to_string(),
223            Utc::now(),
224            "linky_ores".to_string(),
225        )
226        .unwrap();
227
228        let dto: IoTReadingResponseDto = reading.clone().into();
229
230        assert_eq!(dto.id, reading.id);
231        assert_eq!(dto.value, 150.5);
232        assert_eq!(dto.normalized_value, 150.5);
233        assert_eq!(dto.metric_type, MetricType::ElectricityConsumption);
234    }
235
236    #[test]
237    fn test_linky_device_response_dto_from_entity() {
238        let device = LinkyDevice::new(
239            Uuid::new_v4(),
240            "12345678901234".to_string(),
241            LinkyProvider::Enedis,
242            "encrypted_token".to_string(),
243        )
244        .unwrap();
245
246        let dto: LinkyDeviceResponseDto = device.clone().into();
247
248        assert_eq!(dto.id, device.id);
249        assert_eq!(dto.prm, "12345678901234");
250        assert_eq!(dto.provider, LinkyProvider::Enedis);
251        assert_eq!(dto.api_endpoint, "https://ext.hml.myelectricaldata.fr/v1");
252        assert!(dto.needs_sync); // Never synced
253    }
254
255    #[test]
256    fn test_query_iot_readings_dto_default() {
257        let query = QueryIoTReadingsDto::default();
258
259        assert_eq!(query.limit, Some(1000));
260        assert!(query.device_type.is_none());
261        assert!(query.metric_type.is_none());
262
263        let days_diff = (query.end_date - query.start_date).num_days();
264        assert_eq!(days_diff, 30);
265    }
266}