phytrace_sdk/models/domains/
power.rs

1//! Power Domain - Battery, charging, and power state.
2//!
3//! Contains battery status, charging information, and power consumption data.
4
5use serde::{Deserialize, Serialize};
6use validator::Validate;
7
8use crate::models::enums::{BatteryChemistry, ChargingState, PowerSource};
9
10/// Power domain containing battery and charging information.
11#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
12pub struct PowerDomain {
13    // === Battery State ===
14    /// Battery information
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub battery: Option<Battery>,
17
18    // === Charging State ===
19    /// Charging information
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub charging: Option<Charging>,
22
23    // === Power Consumption ===
24    /// Current power consumption in watts
25    #[serde(skip_serializing_if = "Option::is_none")]
26    #[validate(range(min = 0.0))]
27    pub power_consumption_w: Option<f64>,
28
29    /// Average power consumption in watts
30    #[serde(skip_serializing_if = "Option::is_none")]
31    #[validate(range(min = 0.0))]
32    pub average_power_w: Option<f64>,
33
34    // === Power Source ===
35    /// Current power source
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub power_source: Option<PowerSource>,
38
39    // === Voltage/Current ===
40    /// System voltage in volts
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub voltage_v: Option<f64>,
43
44    /// System current in amps
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub current_a: Option<f64>,
47
48    // === Runtime Estimates ===
49    /// Estimated remaining runtime in minutes
50    #[serde(skip_serializing_if = "Option::is_none")]
51    #[validate(range(min = 0.0))]
52    pub estimated_runtime_min: Option<f64>,
53
54    /// Estimated range remaining in meters
55    #[serde(skip_serializing_if = "Option::is_none")]
56    #[validate(range(min = 0.0))]
57    pub estimated_range_m: Option<f64>,
58}
59
60/// Battery status information.
61#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
62pub struct Battery {
63    /// State of charge percentage (0-100)
64    #[serde(skip_serializing_if = "Option::is_none")]
65    #[validate(range(min = 0.0, max = 100.0))]
66    pub state_of_charge_pct: Option<f64>,
67
68    /// State of health percentage (0-100)
69    #[serde(skip_serializing_if = "Option::is_none")]
70    #[validate(range(min = 0.0, max = 100.0))]
71    pub state_of_health_pct: Option<f64>,
72
73    /// Battery voltage in volts
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub voltage_v: Option<f64>,
76
77    /// Battery current in amps (positive = discharging)
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub current_a: Option<f64>,
80
81    /// Battery temperature in Celsius
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub temperature_c: Option<f64>,
84
85    /// Battery capacity in watt-hours
86    #[serde(skip_serializing_if = "Option::is_none")]
87    #[validate(range(min = 0.0))]
88    pub capacity_wh: Option<f64>,
89
90    /// Remaining capacity in watt-hours
91    #[serde(skip_serializing_if = "Option::is_none")]
92    #[validate(range(min = 0.0))]
93    pub remaining_wh: Option<f64>,
94
95    /// Cycle count
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub cycle_count: Option<u32>,
98
99    /// Battery chemistry
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub chemistry: Option<BatteryChemistry>,
102
103    /// Whether battery is low (threshold-based)
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub is_low: Option<bool>,
106
107    /// Whether battery is critical (threshold-based)
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub is_critical: Option<bool>,
110
111    /// Battery cell voltages (if available)
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub cell_voltages_v: Option<Vec<f64>>,
114
115    /// Battery cell temperatures (if available)
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub cell_temperatures_c: Option<Vec<f64>>,
118}
119
120impl Battery {
121    /// Create a battery with state of charge.
122    pub fn from_soc(soc_pct: f64) -> Self {
123        Self {
124            state_of_charge_pct: Some(soc_pct),
125            is_low: Some(soc_pct < 20.0),
126            is_critical: Some(soc_pct < 10.0),
127            ..Default::default()
128        }
129    }
130
131    /// Builder to add voltage.
132    pub fn with_voltage(mut self, voltage_v: f64) -> Self {
133        self.voltage_v = Some(voltage_v);
134        self
135    }
136
137    /// Builder to add temperature.
138    pub fn with_temperature(mut self, temp_c: f64) -> Self {
139        self.temperature_c = Some(temp_c);
140        self
141    }
142}
143
144/// Charging status information.
145#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
146pub struct Charging {
147    /// Whether currently charging
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub is_charging: Option<bool>,
150
151    /// Current charging state
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub state: Option<ChargingState>,
154
155    /// Charging power in watts
156    #[serde(skip_serializing_if = "Option::is_none")]
157    #[validate(range(min = 0.0))]
158    pub power_w: Option<f64>,
159
160    /// Charging current in amps
161    #[serde(skip_serializing_if = "Option::is_none")]
162    #[validate(range(min = 0.0))]
163    pub current_a: Option<f64>,
164
165    /// Charging voltage in volts
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub voltage_v: Option<f64>,
168
169    /// Estimated time to full charge in minutes
170    #[serde(skip_serializing_if = "Option::is_none")]
171    #[validate(range(min = 0.0))]
172    pub time_to_full_min: Option<f64>,
173
174    /// Charger type
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub charger_type: Option<String>,
177
178    /// Charger ID
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub charger_id: Option<String>,
181
182    /// Whether connected to charger
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub is_connected: Option<bool>,
185
186    /// Whether docked at charging station
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub is_docked: Option<bool>,
189
190    /// Charging station ID
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub station_id: Option<String>,
193}
194
195impl Charging {
196    /// Create charging info for actively charging.
197    pub fn active(power_w: f64) -> Self {
198        Self {
199            is_charging: Some(true),
200            state: Some(ChargingState::Charging),
201            power_w: Some(power_w),
202            is_connected: Some(true),
203            ..Default::default()
204        }
205    }
206
207    /// Create charging info for not charging.
208    pub fn not_charging() -> Self {
209        Self {
210            is_charging: Some(false),
211            state: Some(ChargingState::NotCharging),
212            is_connected: Some(false),
213            ..Default::default()
214        }
215    }
216}
217
218impl PowerDomain {
219    /// Create a power domain with battery SOC.
220    pub fn from_soc(soc_pct: f64) -> Self {
221        Self {
222            battery: Some(Battery::from_soc(soc_pct)),
223            ..Default::default()
224        }
225    }
226
227    /// Create a power domain with full battery info.
228    pub fn from_battery(battery: Battery) -> Self {
229        Self {
230            battery: Some(battery),
231            ..Default::default()
232        }
233    }
234
235    /// Builder to add charging info.
236    pub fn with_charging(mut self, charging: Charging) -> Self {
237        self.charging = Some(charging);
238        self
239    }
240
241    /// Builder to add power consumption.
242    pub fn with_consumption(mut self, power_w: f64) -> Self {
243        self.power_consumption_w = Some(power_w);
244        self
245    }
246
247    /// Builder to add runtime estimate.
248    pub fn with_runtime_estimate(mut self, minutes: f64) -> Self {
249        self.estimated_runtime_min = Some(minutes);
250        self
251    }
252
253    /// Convenience: get SOC if available.
254    pub fn soc_pct(&self) -> Option<f64> {
255        self.battery.as_ref().and_then(|b| b.state_of_charge_pct)
256    }
257
258    /// Convenience: check if charging.
259    pub fn is_charging(&self) -> bool {
260        self.charging
261            .as_ref()
262            .and_then(|c| c.is_charging)
263            .unwrap_or(false)
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn test_battery_from_soc() {
273        let battery = Battery::from_soc(75.0);
274        assert_eq!(battery.state_of_charge_pct, Some(75.0));
275        assert_eq!(battery.is_low, Some(false));
276        assert_eq!(battery.is_critical, Some(false));
277
278        let low_battery = Battery::from_soc(15.0);
279        assert_eq!(low_battery.is_low, Some(true));
280        assert_eq!(low_battery.is_critical, Some(false));
281    }
282
283    #[test]
284    fn test_power_domain() {
285        let power = PowerDomain::from_soc(80.0)
286            .with_charging(Charging::not_charging())
287            .with_consumption(50.0);
288
289        assert_eq!(power.soc_pct(), Some(80.0));
290        assert!(!power.is_charging());
291        assert_eq!(power.power_consumption_w, Some(50.0));
292    }
293
294    #[test]
295    fn test_power_serialization() {
296        let power = PowerDomain::from_soc(65.0);
297        let json = serde_json::to_string(&power).unwrap();
298        assert!(json.contains("65"));
299    }
300}