Skip to main content

rointe_core/models/
device.rs

1use serde::{Deserialize, Serialize};
2
3use super::enums::{DeviceMode, DeviceStatus};
4
5/// Full device object as returned by `/devices/{id}.json`.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct RointeDevice {
8    /// All mutable device state (temperatures, mode, schedule, etc.).
9    pub data: DeviceData,
10    /// Device serial number.
11    pub serialnumber: Option<String>,
12    /// Firmware information reported by the device.
13    pub firmware: Option<FirmwareInfo>,
14}
15
16/// The `/data` sub-object containing all mutable device state.
17///
18/// This is both the read model (returned by `GET /devices/{id}.json`) and
19/// the basis for write operations (`PATCH /devices/{id}/data.json`).
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct DeviceData {
22    /// Human-readable device name as configured in the Rointe app.
23    pub name: String,
24
25    /// Physical device type (e.g. `"radiator"`, `"towel"`).
26    ///
27    /// Serialised as the JSON field `"type"`.
28    #[serde(rename = "type")]
29    pub device_type: String,
30
31    /// Hardware generation: `"v1"` or `"v2"`. Absent on some older devices.
32    pub product_version: Option<String>,
33
34    /// Rated power output in watts.
35    pub nominal_power: Option<u32>,
36
37    /// Whether the device is powered on.
38    pub power: bool,
39
40    /// Current operating mode (`manual` or `auto`).
41    pub mode: DeviceMode,
42
43    /// Current active preset or status.
44    pub status: DeviceStatus,
45
46    /// Current target temperature in °C.
47    pub temp: f64,
48
49    /// Internally calculated temperature in °C (device-side computation).
50    pub temp_calc: Option<f64>,
51
52    /// Measured probe temperature in °C (actual room/surface temperature).
53    pub temp_probe: Option<f64>,
54
55    /// Comfort preset temperature in °C.
56    pub comfort: f64,
57
58    /// Eco (energy-saving) preset temperature in °C.
59    pub eco: f64,
60
61    /// Frost-protection temperature in °C.
62    pub ice: f64,
63
64    /// Whether frost-protection (ice) mode is currently active.
65    pub ice_mode: bool,
66
67    /// Weekly schedule: 7 strings (Monday–Sunday), each 24 characters.
68    ///
69    /// Each character represents one hour of the day:
70    /// - `'C'` — comfort temperature
71    /// - `'E'` — eco temperature
72    /// - `'O'` or other — off
73    pub schedule: Option<Vec<String>>,
74
75    /// Current day index within the schedule (0 = Monday).
76    pub schedule_day: Option<u8>,
77
78    /// Current hour index within the schedule.
79    pub schedule_hour: Option<u8>,
80
81    /// v2 only: upper bound for user-adjustable temperature in °C.
82    pub um_max_temp: Option<f64>,
83
84    /// v2 only: lower bound for user-adjustable temperature in °C.
85    pub um_min_temp: Option<f64>,
86
87    /// v2 only: whether user-mode (custom temp bounds) is active.
88    pub user_mode: Option<bool>,
89
90    /// Epoch milliseconds of the last app-side update.
91    ///
92    /// **Must be included in every PATCH request** with the current
93    /// timestamp (`chrono::Utc::now().timestamp_millis()`).
94    pub last_sync_datetime_app: i64,
95
96    /// Epoch milliseconds of the last device-side sync.
97    pub last_sync_datetime_device: Option<i64>,
98}
99
100/// Firmware version information as reported by the device.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct FirmwareInfo {
103    /// Firmware version string installed on the device (e.g. `"3.2.1"`).
104    pub firmware_version_device: Option<String>,
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    const SAMPLE_DEVICE_JSON: &str = r#"{
112        "data": {
113            "name": "Kitchen Radiator",
114            "type": "radiator",
115            "product_version": "v2",
116            "nominal_power": 1500,
117            "power": true,
118            "mode": "manual",
119            "status": "comfort",
120            "temp": 21.5,
121            "temp_calc": 21.3,
122            "temp_probe": 21.2,
123            "comfort": 21.0,
124            "eco": 18.0,
125            "ice": 8.0,
126            "ice_mode": false,
127            "schedule": [
128                "CCCCCCCCEEEEEEEEEEEEEECC",
129                "CCCCCCCCEEEEEEEEEEEEEECC",
130                "CCCCCCCCEEEEEEEEEEEEEECC",
131                "CCCCCCCCEEEEEEEEEEEEEECC",
132                "CCCCCCCCEEEEEEEEEEEEEECC",
133                "CCCCCCCCCCCCCCCCCCCCCCCC",
134                "CCCCCCCCCCCCCCCCCCCCCCCC"
135            ],
136            "schedule_day": 0,
137            "schedule_hour": 0,
138            "um_max_temp": 30.0,
139            "um_min_temp": 7.0,
140            "user_mode": false,
141            "last_sync_datetime_app": 1708360000000,
142            "last_sync_datetime_device": 1708359000000
143        },
144        "serialnumber": "ROINTE12345",
145        "firmware": {
146            "firmware_version_device": "3.2.1"
147        }
148    }"#;
149
150    #[test]
151    fn test_deserialize_full_device() {
152        let device: RointeDevice = serde_json::from_str(SAMPLE_DEVICE_JSON).unwrap();
153
154        assert_eq!(device.data.name, "Kitchen Radiator");
155        assert_eq!(device.data.device_type, "radiator");
156        assert_eq!(device.data.temp, 21.5);
157        assert_eq!(device.data.comfort, 21.0);
158        assert_eq!(device.data.eco, 18.0);
159        assert_eq!(device.data.ice, 8.0);
160        assert!(device.data.power);
161        assert!(!device.data.ice_mode);
162        assert_eq!(device.data.mode, DeviceMode::Manual);
163        assert_eq!(device.data.status, DeviceStatus::Comfort);
164
165        let schedule = device.data.schedule.unwrap();
166        assert_eq!(schedule.len(), 7);
167        assert_eq!(schedule[0], "CCCCCCCCEEEEEEEEEEEEEECC");
168
169        assert_eq!(device.serialnumber.as_deref(), Some("ROINTE12345"));
170        let fw = device.firmware.unwrap();
171        assert_eq!(fw.firmware_version_device.as_deref(), Some("3.2.1"));
172    }
173
174    #[test]
175    fn test_deserialize_device_status_none() {
176        let json = r#"{
177            "data": {
178                "name": "Bedroom",
179                "type": "radiator",
180                "power": true,
181                "mode": "manual",
182                "status": "none",
183                "temp": 20.0,
184                "comfort": 21.0,
185                "eco": 18.0,
186                "ice": 8.0,
187                "ice_mode": false,
188                "last_sync_datetime_app": 1708360000000
189            }
190        }"#;
191
192        let device: RointeDevice = serde_json::from_str(json).unwrap();
193        assert_eq!(device.data.status, DeviceStatus::NoStatus);
194    }
195
196    #[test]
197    fn test_deserialize_auto_mode() {
198        let json = r#"{
199            "data": {
200                "name": "Hall",
201                "type": "radiator",
202                "power": true,
203                "mode": "auto",
204                "status": "eco",
205                "temp": 18.0,
206                "comfort": 21.0,
207                "eco": 18.0,
208                "ice": 8.0,
209                "ice_mode": false,
210                "last_sync_datetime_app": 1708360000000
211            }
212        }"#;
213
214        let device: RointeDevice = serde_json::from_str(json).unwrap();
215        assert_eq!(device.data.mode, DeviceMode::Auto);
216    }
217}