1use serde::{Deserialize, Deserializer};
2use std::collections::HashMap;
3use uom::si::{
4 energy::watt_hour,
5 f64::{Energy, Power},
6 power::{kilowatt, watt},
7};
8
9pub const REFRESH_TIME_IN_M: i64 = 15;
10
11#[derive(Debug, Clone, Deserialize)]
12pub(crate) struct SitesReply {
13 sites: Sites,
14}
15
16impl SitesReply {
17 pub fn sites(&self) -> &Vec<Site> {
18 &self.sites.site
19 }
20}
21
22#[derive(Debug, Clone, Deserialize)]
23pub struct Sites {
24 #[serde(rename = "count")]
25 _count: u32,
26 site: Vec<Site>,
27}
28
29#[derive(Debug, Clone, Deserialize)]
30pub struct SiteDetails {
31 pub details: Site,
32}
33
34#[derive(Debug, Clone, Deserialize)]
35pub struct Site {
36 pub id: u32,
38 pub name: String,
40 #[serde(rename = "accountId")]
42 pub account_id: u32,
43 pub status: String,
45 #[serde(rename = "peakPower", deserialize_with = "parse_power_kw")]
47 pub peak_power: Power,
48 #[serde(rename = "lastUpdateTime", deserialize_with = "parse_date")]
49 pub last_update_time: chrono::NaiveDate,
50 #[serde(rename = "installationDate", deserialize_with = "parse_date")]
52 pub installation_date: chrono::NaiveDate,
53 #[serde(rename = "ptoDate")]
55 pub pto_date: Option<String>,
56 pub notes: String,
57 #[serde(rename = "type")]
59 pub site_type: String,
60 pub location: Location,
62 #[serde(rename = "primaryModule")]
63 pub primary_module: PrimaryModule,
64 pub uris: HashMap<String, String>,
65 #[serde(rename = "publicSettings")]
67 pub public_settings: PublicSettings,
68}
69
70#[derive(Debug, Clone, Deserialize)]
72pub struct Location {
73 pub country: String,
74 pub city: String,
75 pub address: String,
76 pub zip: String,
77 #[serde(rename = "timeZone")]
78 pub time_zone: String,
79 #[serde(rename = "countryCode")]
80 pub country_code: String,
81}
82
83#[derive(Debug, Clone, Deserialize)]
85pub struct PrimaryModule {
86 #[serde(rename = "manufacturerName")]
87 pub manufacturer_name: String,
88 #[serde(rename = "modelName")]
89 pub model_name: String,
90 #[serde(rename = "maximumPower", deserialize_with = "parse_power_kw")]
91 pub maximum_power: Power,
92 #[serde(rename = "temperatureCoef")]
93 pub temperature_coef: f32,
94}
95
96#[derive(Debug, Clone, Deserialize)]
98pub struct PublicSettings {
99 #[serde(rename = "isPublic")]
100 pub public: bool,
101}
102
103#[derive(Debug, Clone, Deserialize)]
105pub struct DataPeriod {
106 #[serde(rename = "startDate", deserialize_with = "parse_date")]
107 pub start_date: chrono::NaiveDate,
108 #[serde(rename = "endDate", deserialize_with = "parse_date")]
109 pub end_date: chrono::NaiveDate,
110}
111
112impl DataPeriod {
113 pub fn formatted_start_date(&self) -> String {
116 Self::formatted_date(&self.start_date)
117 }
118
119 pub fn formatted_end_date(&self) -> String {
122 Self::formatted_date(&self.end_date)
123 }
124
125 fn formatted_date(date: &chrono::NaiveDate) -> String {
126 date.format("%Y-%m-%d").to_string()
127 }
128}
129
130#[derive(Debug, Clone, Deserialize)]
131pub(crate) struct DataPeriodReply {
132 #[serde(rename = "dataPeriod")]
133 pub(crate) data_period: DataPeriod,
134}
135
136#[derive(Debug, Clone, Deserialize)]
137pub(crate) struct OverviewReply {
138 pub(crate) overview: Overview,
139}
140
141#[derive(Debug, Clone, Deserialize)]
143pub struct Overview {
144 #[serde(rename = "lastUpdateTime", deserialize_with = "parse_date_time")]
145 pub last_updated_time: chrono::NaiveDateTime,
146 #[serde(rename = "lifeTimeData")]
147 pub life_time_data: TimeData,
148 #[serde(rename = "lastYearData")]
149 pub last_year_data: TimeData,
150 #[serde(rename = "lastMonthData")]
151 pub last_month_data: TimeData,
152 #[serde(rename = "lastDayData")]
153 pub last_day_data: TimeData,
154 #[serde(rename = "currentPower")]
155 pub current_power: GeneratedPowerW,
156 #[serde(rename = "measuredBy")]
157 pub measured_by: String,
158}
159
160impl Overview {
161 pub fn estimated_next_update(&self) -> (chrono::NaiveDateTime, chrono::Duration) {
165 let next = self.last_updated_time + chrono::Duration::seconds(REFRESH_TIME_IN_M * 60 + 10);
167 let delta = next - chrono::Local::now().naive_local();
168 (next, delta)
169 }
170}
171
172#[derive(Debug, Clone, Deserialize)]
174pub struct TimeData {
175 #[serde(deserialize_with = "parse_energy_wh")]
176 pub energy: Energy,
177 pub revenue: Option<f32>,
178}
179
180#[derive(Debug, Clone, Deserialize)]
182pub struct GeneratedPower {
183 #[serde(deserialize_with = "parse_power_kw")]
184 pub power: Power,
185}
186
187#[derive(Debug, Clone, Deserialize)]
189pub struct GeneratedPowerW {
190 #[serde(deserialize_with = "parse_power_w")]
191 pub power: Power,
192}
193
194#[derive(Debug, Clone, Deserialize)]
195pub enum TimeUnit {
196 QuarterOfAnHour,
197 Hour,
198 Day,
199 Week,
200 Month,
201 Year,
202}
203
204const QUARTER_OF_AN_HOUR: &str = "QUARTER_OF_AN_HOUR";
205const HOUR: &str = "HOUR";
206const DAY: &str = "DAY";
207const WEEK: &str = "WEEK";
208const MONTH: &str = "MONTH";
209const YEAR: &str = "YEAR";
210
211impl TimeUnit {
212 pub fn to_param(&self) -> &'static str {
213 match self {
214 TimeUnit::QuarterOfAnHour => QUARTER_OF_AN_HOUR,
215 TimeUnit::Hour => HOUR,
216 TimeUnit::Day => DAY,
217 TimeUnit::Week => WEEK,
218 TimeUnit::Month => MONTH,
219 TimeUnit::Year => YEAR,
220 }
221 }
222
223 pub fn from_const<'de, D>(deserializer: D) -> Result<TimeUnit, D::Error>
224 where
225 D: Deserializer<'de>,
226 {
227 let s: String = String::deserialize(deserializer)?;
228 match s.as_str() {
229 QUARTER_OF_AN_HOUR => Ok(TimeUnit::QuarterOfAnHour),
230 HOUR => Ok(TimeUnit::Hour),
231 DAY => Ok(TimeUnit::Day),
232 WEEK => Ok(TimeUnit::Week),
233 MONTH => Ok(TimeUnit::Month),
234 YEAR => Ok(TimeUnit::Year),
235 _ => Err(serde::de::Error::custom("Cannot parse value")),
236 }
237 }
238}
239
240#[derive(Debug, Clone, Deserialize)]
241pub(crate) struct GeneratedEnergyReply {
242 pub(crate) energy: GeneratedEnergy,
243}
244
245#[derive(Debug, Clone, Deserialize)]
247pub struct GeneratedEnergy {
248 #[serde(rename = "timeUnit", deserialize_with = "TimeUnit::from_const")]
249 pub time_unit: TimeUnit,
250 unit: String,
251 values: Vec<RawGeneratedEnergyValue>,
252}
253
254impl GeneratedEnergy {
255 pub fn values(&self) -> Vec<GeneratedEnergyValue> {
257 self.values
258 .iter()
259 .map(|raw| raw.convert(&self.unit))
260 .collect()
261 }
262}
263
264#[derive(Debug, Clone, Deserialize, Copy)]
268struct RawGeneratedEnergyValue {
269 #[serde(deserialize_with = "parse_date_time")]
270 date: chrono::NaiveDateTime,
271 value: Option<f64>,
272}
273
274impl RawGeneratedEnergyValue {
275 fn convert(&self, unit: &str) -> GeneratedEnergyValue {
278 let value = match unit {
279 "Wh" => self.value.map(Energy::new::<watt_hour>),
280 _ => todo!("unsupported unit: {unit}"),
281 };
282 GeneratedEnergyValue {
283 date: self.date,
284 value,
285 }
286 }
287}
288
289#[derive(Debug, Clone, Copy)]
292pub struct GeneratedEnergyValue {
293 pub date: chrono::NaiveDateTime,
295 pub value: Option<Energy>,
298}
299
300#[derive(Debug, Clone, Deserialize)]
302pub(crate) struct GeneratedPowerReply {
303 pub(crate) power: GeneratedPowerPerTimeUnit,
304}
305
306#[derive(Debug, Clone, Deserialize)]
308pub struct GeneratedPowerPerTimeUnit {
309 #[serde(rename = "timeUnit", deserialize_with = "TimeUnit::from_const")]
310 pub time_unit: TimeUnit,
311 unit: String,
312 values: Vec<RawGeneratedPowerValue>,
313}
314
315impl GeneratedPowerPerTimeUnit {
316 pub fn values(&self) -> Vec<GeneratedPowerValue> {
318 self.values
319 .iter()
320 .map(|raw| raw.convert(&self.unit))
321 .collect()
322 }
323}
324
325#[derive(Debug, Clone, Deserialize)]
326struct RawGeneratedPowerValue {
327 #[serde(deserialize_with = "parse_date_time")]
328 date: chrono::NaiveDateTime,
329 value: Option<f64>,
330}
331
332impl RawGeneratedPowerValue {
333 pub fn convert(&self, unit: &str) -> GeneratedPowerValue {
336 let value: Option<Power> = match unit {
337 "W" => self.value.map(Power::new::<watt>),
338 _ => todo!("unsupported unit: {unit}"),
339 };
340 GeneratedPowerValue {
341 date: self.date,
342 value,
343 }
344 }
345}
346
347#[derive(Debug, Clone)]
350pub struct GeneratedPowerValue {
351 pub date: chrono::NaiveDateTime,
352 pub value: Option<Power>,
353}
354
355fn parse_date_time<'de, D>(deserializer: D) -> Result<chrono::NaiveDateTime, D::Error>
357where
358 D: Deserializer<'de>,
359{
360 let s: String = String::deserialize(deserializer)?;
361 chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S")
362 .map_err(|_| serde::de::Error::custom("Cannot parse value"))
363}
364
365fn parse_date<'de, D>(deserializer: D) -> Result<chrono::NaiveDate, D::Error>
367where
368 D: Deserializer<'de>,
369{
370 let s: String = String::deserialize(deserializer)?;
371 chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d")
372 .map_err(|_| serde::de::Error::custom("Cannot parse value"))
373}
374
375fn parse_power_kw<'de, D>(deserializer: D) -> Result<Power, D::Error>
377where
378 D: Deserializer<'de>,
379{
380 let value: f64 = f64::deserialize(deserializer)?;
381 Ok(Power::new::<kilowatt>(value))
382}
383
384fn parse_power_w<'de, D>(deserializer: D) -> Result<Power, D::Error>
386where
387 D: Deserializer<'de>,
388{
389 let value: f64 = f64::deserialize(deserializer)?;
390 Ok(Power::new::<watt>(value))
391}
392
393fn parse_energy_wh<'de, D>(deserializer: D) -> Result<Energy, D::Error>
395where
396 D: Deserializer<'de>,
397{
398 let value: f64 = f64::deserialize(deserializer)?;
399 Ok(Energy::new::<watt_hour>(value))
400}
401
402#[test]
403fn test_parse_sites_data() {
404 let output = r#"
405 {"sites":{
406 "count":1,
407 "site":[
408 {"id":1234123,
409 "name":"MySiteName",
410 "accountId":123456,
411 "status":"Active",
412 "peakPower":7.41,
413 "lastUpdateTime":"2021-04-29",
414 "installationDate":"2021-02-25",
415 "ptoDate":null,
416 "notes":"",
417 "type":"Optimizers & Inverters",
418 "location":{
419 "country":"Netherlands",
420 "city":"A city",
421 "address":"Some address",
422 "zip":"zipy",
423 "timeZone":"Europe/Amsterdam",
424 "countryCode":"NL"
425 },
426 "primaryModule":{
427 "manufacturerName":"JinkoSolar",
428 "modelName":"390",
429 "maximumPower":0.0,
430 "temperatureCoef":0.0
431 },
432 "uris":{
433 "SITE_IMAGE":"/site/1234123/siteImage/file12341234.jpg",
434 "DATA_PERIOD":"/site/1234123/dataPeriod",
435 "DETAILS":"/site/1234123/details",
436 "OVERVIEW":"/site/1234123/overview"
437 },
438 "publicSettings":{
439 "isPublic":false
440 }}
441 ]
442 }
443 }"#;
444
445 let reply: SitesReply = serde_json::from_str(output).unwrap();
446 println!("{:?}", reply);
447 assert_eq!(reply.sites._count, 1);
448 let power = Power::new::<kilowatt>(7.41);
449 assert_eq!(power, reply.sites.site[0].peak_power);
450}
451
452#[test]
453fn test_parse_data_period() {
454 let reply = r#"{"dataPeriod":{"startDate":"2021-02-25","endDate":"2021-05-03"}}"#;
455 println!("{}", reply);
456 let parsed: DataPeriodReply = serde_json::from_str(reply).unwrap();
457 assert_eq!("2021-02-25", parsed.data_period.formatted_start_date());
458 assert_eq!("2021-05-03", parsed.data_period.formatted_end_date());
459}
460
461#[test]
462fn test_energy() {
463 use uom::si::energy::watt_hour;
464
465 let reply = r#"
466 {"energy":{
467 "timeUnit":"MONTH",
468 "unit":"Wh",
469 "measuredBy":"INVERTER",
470 "values":[
471 {"date":"2021-02-01 00:00:00","value":45718.0},
472 {"date":"2021-03-01 00:00:00","value":504857.0},
473 {"date":"2021-04-01 00:00:00","value":800476.0},
474 {"date":"2021-05-01 00:00:00","value":89913.0}]}}
475 "#;
476
477 let parsed: GeneratedEnergyReply = serde_json::from_str(reply).unwrap();
478 assert_eq!(
479 45718.0,
480 parsed.energy.values()[0]
481 .value
482 .map(|e| e.get::<watt_hour>())
483 .unwrap()
484 );
485}
486
487#[test]
488fn test_overview() {
489 let reply = r#"
490 {"overview":{
491 "lastUpdateTime":"2023-11-09 10:28:56",
492 "lifeTimeData":{"energy":1.9191678E7},
493 "lastYearData":{"energy":6143745.0},
494 "lastMonthData":{"energy":38709.0},
495 "lastDayData":{"energy":2028.0},
496 "currentPower":{"power":1173.7279},
497 "measuredBy":"INVERTER"}
498 }
499 "#;
500
501 let parsed: OverviewReply = serde_json::from_str(reply).unwrap();
502 assert_eq!(
503 Energy::new::<watt_hour>(1.9191678E7),
504 parsed.overview.life_time_data.energy
505 );
506 assert_eq!(
507 Power::new::<watt>(1173.7279),
508 parsed.overview.current_power.power
509 );
510}
511
512#[test]
513fn test_energy_in_period() {
514 let reply = r#"
515 {"energy":{
516 "timeUnit":"HOUR",
517 "unit":"Wh",
518 "measuredBy":"INVERTER",
519 "values":[
520 {"date":"2023-11-09 00:00:00","value":null},
521 {"date":"2023-11-09 01:00:00","value":null},
522 {"date":"2023-11-09 02:00:00","value":null},
523 {"date":"2023-11-09 03:00:00","value":null},
524 {"date":"2023-11-09 04:00:00","value":0.0},
525 {"date":"2023-11-09 05:00:00","value":null},
526 {"date":"2023-11-09 06:00:00","value":null},
527 {"date":"2023-11-09 07:00:00","value":0.0},
528 {"date":"2023-11-09 08:00:00","value":256.0},
529 {"date":"2023-11-09 09:00:00","value":827.0},
530 {"date":"2023-11-09 10:00:00","value":1390.0},
531 {"date":"2023-11-09 11:00:00","value":222.0},
532 {"date":"2023-11-09 12:00:00","value":null},
533 {"date":"2023-11-09 13:00:00","value":null},
534 {"date":"2023-11-09 14:00:00","value":null},
535 {"date":"2023-11-09 15:00:00","value":null},
536 {"date":"2023-11-09 16:00:00","value":null},
537 {"date":"2023-11-09 17:00:00","value":null},
538 {"date":"2023-11-09 18:00:00","value":null},
539 {"date":"2023-11-09 19:00:00","value":null},
540 {"date":"2023-11-09 20:00:00","value":null},
541 {"date":"2023-11-09 21:00:00","value":null},
542 {"date":"2023-11-09 22:00:00","value":null},
543 {"date":"2023-11-09 23:00:00","value":null}
544 ]
545 }
546 }
547 "#;
548
549 let parsed: GeneratedEnergyReply = serde_json::from_str(reply).unwrap();
550 assert_eq!(24, parsed.energy.values().len());
551 assert_eq!(
552 Some(Energy::new::<watt_hour>(222.0)),
553 parsed.energy.values()[11].value
554 );
555}
556
557#[test]
558fn test_power_in_period() {
559 let reply = r#"
560 {"power":{
561 "timeUnit":"QUARTER_OF_AN_HOUR",
562 "unit":"W",
563 "measuredBy":"INVERTER",
564 "values":[
565 {"date":"2023-11-09 12:15:00","value":761.538},
566 {"date":"2023-11-09 12:30:00","value":822.26117},
567 {"date":"2023-11-09 12:45:00","value":746.9589},
568 {"date":"2023-11-09 13:00:00","value":563.11},
569 {"date":"2023-11-09 13:15:00","value":554.06836}
570 ]
571 }}
572 "#;
573
574 let parsed: GeneratedPowerReply = serde_json::from_str(reply).unwrap();
575 assert_eq!(5, parsed.power.values().len());
576 assert_eq!(
577 Some(Power::new::<watt>(761.538)),
578 parsed.power.values()[0].value
579 );
580}