phytrace_sdk/models/domains/
power.rs1use serde::{Deserialize, Serialize};
6use validator::Validate;
7
8use crate::models::enums::{BatteryChemistry, ChargingState, PowerSource};
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
12pub struct PowerDomain {
13 #[serde(skip_serializing_if = "Option::is_none")]
16 pub battery: Option<Battery>,
17
18 #[serde(skip_serializing_if = "Option::is_none")]
21 pub charging: Option<Charging>,
22
23 #[serde(skip_serializing_if = "Option::is_none")]
26 #[validate(range(min = 0.0))]
27 pub power_consumption_w: Option<f64>,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 #[validate(range(min = 0.0))]
32 pub average_power_w: Option<f64>,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
37 pub power_source: Option<PowerSource>,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
42 pub voltage_v: Option<f64>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub current_a: Option<f64>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
51 #[validate(range(min = 0.0))]
52 pub estimated_runtime_min: Option<f64>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 #[validate(range(min = 0.0))]
57 pub estimated_range_m: Option<f64>,
58}
59
60#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
62pub struct Battery {
63 #[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 #[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 #[serde(skip_serializing_if = "Option::is_none")]
75 pub voltage_v: Option<f64>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub current_a: Option<f64>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub temperature_c: Option<f64>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 #[validate(range(min = 0.0))]
88 pub capacity_wh: Option<f64>,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
92 #[validate(range(min = 0.0))]
93 pub remaining_wh: Option<f64>,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub cycle_count: Option<u32>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub chemistry: Option<BatteryChemistry>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub is_low: Option<bool>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub is_critical: Option<bool>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub cell_voltages_v: Option<Vec<f64>>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub cell_temperatures_c: Option<Vec<f64>>,
118}
119
120impl Battery {
121 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 pub fn with_voltage(mut self, voltage_v: f64) -> Self {
133 self.voltage_v = Some(voltage_v);
134 self
135 }
136
137 pub fn with_temperature(mut self, temp_c: f64) -> Self {
139 self.temperature_c = Some(temp_c);
140 self
141 }
142}
143
144#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
146pub struct Charging {
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub is_charging: Option<bool>,
150
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub state: Option<ChargingState>,
154
155 #[serde(skip_serializing_if = "Option::is_none")]
157 #[validate(range(min = 0.0))]
158 pub power_w: Option<f64>,
159
160 #[serde(skip_serializing_if = "Option::is_none")]
162 #[validate(range(min = 0.0))]
163 pub current_a: Option<f64>,
164
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub voltage_v: Option<f64>,
168
169 #[serde(skip_serializing_if = "Option::is_none")]
171 #[validate(range(min = 0.0))]
172 pub time_to_full_min: Option<f64>,
173
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub charger_type: Option<String>,
177
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub charger_id: Option<String>,
181
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub is_connected: Option<bool>,
185
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub is_docked: Option<bool>,
189
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub station_id: Option<String>,
193}
194
195impl Charging {
196 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 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 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 pub fn from_battery(battery: Battery) -> Self {
229 Self {
230 battery: Some(battery),
231 ..Default::default()
232 }
233 }
234
235 pub fn with_charging(mut self, charging: Charging) -> Self {
237 self.charging = Some(charging);
238 self
239 }
240
241 pub fn with_consumption(mut self, power_w: f64) -> Self {
243 self.power_consumption_w = Some(power_w);
244 self
245 }
246
247 pub fn with_runtime_estimate(mut self, minutes: f64) -> Self {
249 self.estimated_runtime_min = Some(minutes);
250 self
251 }
252
253 pub fn soc_pct(&self) -> Option<f64> {
255 self.battery.as_ref().and_then(|b| b.state_of_charge_pct)
256 }
257
258 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}