1use jiff::{
4 Span, Timestamp,
5 civil::{Date, Time},
6};
7use reqwest::header::HeaderMap;
8use serde::{
9 Deserialize, Deserializer,
10 de::{DeserializeOwned, Error},
11};
12
13use crate::types::{Conditions, error::RateLimitError, regions::Region};
14
15#[derive(Debug, Clone)]
17pub struct Response<T> {
18 pub data: T,
20
21 pub rate_limit: RateLimit,
23}
24
25impl<T> Response<T> {
26 pub(crate) const fn new(data: T, rate_limit: RateLimit) -> Self { Self { data, rate_limit } }
27}
28
29#[derive(Debug, Clone)]
34pub struct RateLimit {
35 pub limit: u16,
37
38 pub remaining: u16,
40
41 pub reset: Timestamp,
43}
44
45impl RateLimit {
46 #[must_use]
56 pub const fn used(&self) -> u16 {
57 debug_assert!(self.remaining <= self.limit);
58 self.limit - self.remaining
59 }
60}
61
62impl TryFrom<&HeaderMap> for RateLimit {
63 type Error = RateLimitError;
64
65 fn try_from(value: &HeaderMap) -> Result<Self, Self::Error> {
66 if let (Some(limit), Some(remaining), Some(reset)) = (
67 value.get("X-Rate-Limit-Limit"),
68 value.get("X-Rate-Limit-Remaining"),
69 value.get("X-Rate-Limit-Reset"),
70 ) {
71 let limit = limit.to_str()?.parse()?;
72 let remaining = remaining.to_str()?.parse()?;
73 let reset = Timestamp::from_second(reset.to_str()?.parse()?)?;
74
75 debug_assert!(remaining <= limit);
76
77 Ok(Self {
78 limit,
79 remaining,
80 reset,
81 })
82 } else {
83 Err(RateLimitError::MissingValues)
84 }
85 }
86}
87
88#[derive(Debug, Clone, Deserialize)]
90pub struct Status {
91 pub date: Date,
93
94 pub time: Time,
96
97 pub energy_generation: f32,
99
100 pub power_generation: f32,
102
103 #[serde(deserialize_with = "opt_nan_f32")]
105 pub energy_consumption: Option<f32>,
106
107 #[serde(deserialize_with = "opt_nan_f32")]
109 pub power_consumption: Option<f32>,
110
111 pub normalised_output: f32,
114
115 #[serde(deserialize_with = "opt_nan_f32")]
117 pub temperature: Option<f32>,
118
119 #[serde(deserialize_with = "opt_nan_f32")]
121 pub voltage: Option<f32>,
122
123 #[serde(default)]
125 pub extended_value_1: Option<f32>,
126
127 #[serde(default)]
129 pub extended_value_2: Option<f32>,
130
131 #[serde(default)]
133 pub extended_value_3: Option<f32>,
134
135 #[serde(default)]
137 pub extended_value_4: Option<f32>,
138
139 #[serde(default)]
141 pub extended_value_5: Option<f32>,
142
143 #[serde(default)]
145 pub extended_value_6: Option<f32>,
146}
147
148#[derive(Debug, Clone, Deserialize)]
152pub struct History {
153 pub date: Date,
155
156 pub time: Time,
158
159 pub energy_generation: f32,
161
162 pub energy_efficiency: f32,
164
165 pub instantaneous_power: f32,
167
168 pub average_power: f32,
170
171 pub normalised_output: f32,
174
175 #[serde(deserialize_with = "opt_nan_f32")]
177 pub energy_consumption: Option<f32>,
178
179 #[serde(deserialize_with = "opt_nan_f32")]
181 pub power_consumption: Option<f32>,
182
183 #[serde(deserialize_with = "opt_nan_f32")]
185 pub temperature: Option<f32>,
186
187 #[serde(deserialize_with = "opt_nan_f32")]
189 pub voltage: Option<f32>,
190
191 #[serde(default)]
193 pub extended_value_1: Option<f32>,
194
195 #[serde(default)]
197 pub extended_value_2: Option<f32>,
198
199 #[serde(default)]
201 pub extended_value_3: Option<f32>,
202
203 #[serde(default)]
205 pub extended_value_4: Option<f32>,
206
207 #[serde(default)]
209 pub extended_value_5: Option<f32>,
210
211 #[serde(default)]
213 pub extended_value_6: Option<f32>,
214}
215
216#[derive(Debug, Clone, Deserialize)]
220pub struct DailyStatus {
221 pub energy_generation: f32,
223
224 pub power_generation: f32,
226
227 pub peak_power_generation: f32,
229
230 pub peak_time: Time,
232
233 #[serde(skip)]
235 pub consumption: Option<DailyConsumption>,
236
237 #[serde(skip)]
239 pub temperatures: Option<DailyTemperatures>,
240}
241
242#[derive(Debug, Clone, Deserialize, Default)]
246pub struct DailyConsumption {
247 pub energy_consumption: Option<f32>,
249
250 pub power_consumption: Option<f32>,
252
253 pub standby_power_consumption: Option<f32>,
255
256 pub standby_power_time: Option<Time>,
258}
259
260#[derive(Debug, Clone, Deserialize, Default)]
264pub struct DailyTemperatures {
265 pub lowest_temperature: Option<f32>,
267
268 pub highest_temperature: Option<f32>,
270
271 pub average_temperature: Option<f32>,
273}
274
275#[derive(Debug, Clone, Deserialize)]
279pub struct Statistics {
280 pub energy_generated: u64,
282
283 pub energy_exported: u64,
285
286 pub average_energy_generation: u32,
288
289 pub minimum_energy_generation: u32,
291
292 pub maximum_energy_generation: u32,
294
295 pub average_efficiency: f32,
297
298 pub outputs: u32,
300
301 pub date_from: Date,
303
304 pub date_to: Date,
306
307 pub record_efficiency: f32,
309
310 pub record_date: Date,
312
313 #[serde(skip)]
315 pub consumption: Option<ConsumptionStatistics>,
316
317 #[serde(skip)]
319 pub credit: Option<f32>,
320
321 #[serde(skip)]
323 pub debit: Option<f32>,
324}
325
326#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
331pub struct ConsumptionStatistics {
332 pub energy_consumed: u64,
334
335 pub peak_energy_import: u32,
337
338 pub off_peak_energy_import: u32,
340
341 pub shoulder_energy_import: u32,
343
344 pub high_shoulder_energy_import: u32,
346
347 pub average_consumption: u32,
349
350 pub minimum_consumption: u32,
352
353 pub maximum_consumption: u32,
355}
356
357#[derive(Debug, Clone, Deserialize, Default)]
362#[serde(default)]
363pub struct System {
364 pub name: String,
366
367 pub size: u64,
369
370 pub postcode: Option<String>,
372
373 pub panels: Option<u32>,
375
376 pub panel_power: Option<u32>,
378
379 pub brand: Option<String>,
381
382 pub inverters: Option<u32>,
384
385 pub inverter_power: Option<u64>,
387
388 pub inverter_brand: Option<String>,
390
391 pub orientation: Option<Orientation>,
393
394 #[serde(deserialize_with = "opt_nan_f32")]
396 pub tilt: Option<f32>,
397
398 #[serde(deserialize_with = "nullable")]
400 pub shade: Option<Shade>,
401
402 pub install_date: Option<Date>,
404
405 #[serde(deserialize_with = "opt_nan_f32")]
407 pub latitude: Option<f32>,
408
409 #[serde(deserialize_with = "opt_nan_f32")]
411 pub longitude: Option<f32>,
412}
413
414#[derive(Debug, Clone, Deserialize, Default)]
418pub struct FullSystem {
419 #[serde(skip)]
421 pub system: System,
422
423 pub status_interval: Option<u32>,
425
426 #[serde(default)]
428 pub secondary_panels: Option<u32>,
429
430 #[serde(default)]
432 pub secondary_panel_power: Option<u32>,
433
434 #[serde(deserialize_with = "nullable", default)]
436 pub secondary_orientation: Option<Orientation>,
437
438 #[serde(deserialize_with = "opt_nan_f32", default)]
440 pub secondary_tilt: Option<f32>,
441
442 #[serde(skip)]
444 pub tariffs: Tariffs,
445
446 #[serde(skip)]
448 pub teams: Vec<u32>,
449
450 #[serde(skip)]
452 pub donations: u32,
453
454 #[serde(skip)]
456 pub extended_data: Vec<Option<f32>>,
457
458 #[serde(skip)]
460 pub monthly_estimates: Vec<f32>,
461}
462
463impl From<System> for FullSystem {
464 fn from(value: System) -> Self {
465 Self {
466 system: value,
467 ..Default::default()
468 }
469 }
470}
471
472#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq)]
477#[serde(default)]
478pub struct Tariffs {
479 #[serde(deserialize_with = "opt_nan_f32")]
481 pub export: Option<f32>,
482
483 #[serde(deserialize_with = "opt_nan_f32")]
485 pub import_peak: Option<f32>,
486
487 #[serde(deserialize_with = "opt_nan_f32")]
489 pub import_off_peak: Option<f32>,
490
491 #[serde(deserialize_with = "opt_nan_f32")]
493 pub import_shoulder: Option<f32>,
494
495 #[serde(deserialize_with = "opt_nan_f32")]
497 pub import_high_shoulder: Option<f32>,
498
499 #[serde(deserialize_with = "opt_nan_f32")]
501 pub daily: Option<f32>,
502}
503
504#[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq, Hash)]
506pub enum Orientation {
507 #[serde(rename = "N")]
508 North,
509
510 #[serde(rename = "NE")]
511 NorthEast,
512
513 #[serde(rename = "NW")]
514 NorthWest,
515
516 #[serde(rename = "W")]
517 West,
518
519 #[serde(rename = "E")]
520 East,
521
522 #[serde(rename = "EW")]
523 EastWest,
524
525 #[serde(rename = "S")]
526 South,
527
528 #[serde(rename = "SE")]
529 SouthEast,
530
531 #[serde(rename = "SW")]
532 SouthWest,
533}
534
535#[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq, Hash, PartialOrd, Ord)]
537pub enum Shade {
538 No,
540
541 Low,
543
544 Medium,
546
547 High,
549}
550
551#[derive(Debug, Clone, Deserialize)]
555pub struct Ladder {
556 pub date: Date,
558
559 #[serde(deserialize_with = "opt_nan_u32")]
561 pub generation_rank: Option<u32>,
562
563 #[serde(deserialize_with = "opt_nan_u32")]
565 pub efficiency_rank: Option<u32>,
566
567 #[serde(deserialize_with = "opt_nan_f32")]
569 pub efficiency: Option<f32>,
570
571 #[serde(deserialize_with = "opt_nan_u32")]
573 pub total_outputs: Option<u32>,
574
575 pub last_output: Option<Date>,
577
578 #[serde(deserialize_with = "opt_nan_u64")]
580 pub total_generation: Option<u64>,
581
582 #[serde(deserialize_with = "opt_nan_u64")]
584 pub total_consumption: Option<u64>,
585
586 #[serde(deserialize_with = "opt_nan_u32")]
588 pub max_generation: Option<u32>,
589
590 #[serde(deserialize_with = "opt_nan_u32")]
592 pub max_consumption: Option<u32>,
593
594 #[serde(deserialize_with = "opt_nan_u32")]
596 pub system_age: Option<u32>,
597}
598
599impl Ladder {
600 #[must_use]
602 pub fn generation_rank_url(&self) -> Option<String> {
603 self
604 .generation_rank
605 .map(|rank| format!("https://pvoutput.org/ladder.jsp?rank={rank}&o=e&d=desc&filter=0&p={}", rank / 30))
606 }
607}
608
609#[derive(Debug, Clone, Deserialize)]
613pub struct Output {
614 pub date: Date,
615
616 pub energy_generation: u32,
618
619 pub energy_efficiency: f32,
621
622 pub energy_exported: u32,
624
625 pub energy_used: u32,
627
628 #[serde(deserialize_with = "opt_nan_u32")]
630 pub peak_power: Option<u32>,
631
632 #[serde(deserialize_with = "opt_nan_time")]
634 pub peak_time: Option<Time>,
635
636 pub conditions: Conditions,
638
639 #[serde(deserialize_with = "opt_nan_u32")]
641 pub min_temperature: Option<u32>,
642
643 #[serde(deserialize_with = "opt_nan_u32")]
645 pub max_temperature: Option<u32>,
646
647 #[serde(deserialize_with = "opt_nan_u32")]
649 pub peak_energy_import: Option<u32>,
650
651 #[serde(deserialize_with = "opt_nan_u32")]
653 pub off_peak_energy_import: Option<u32>,
654
655 #[serde(deserialize_with = "opt_nan_u32")]
657 pub shoulder_energy_import: Option<u32>,
658
659 #[serde(deserialize_with = "opt_nan_u32")]
661 pub high_shoulder_energy_import: Option<u32>,
662
663 #[serde(skip)]
664 pub export: Option<OutputExport>,
666
667 #[serde(skip)]
669 pub insolation: Option<u32>,
670}
671
672#[derive(Clone, Debug, Deserialize, Default)]
674pub struct OutputExport {
675 #[serde(deserialize_with = "opt_nan_u32")]
677 pub peak_energy_export: Option<u32>,
678
679 #[serde(deserialize_with = "opt_nan_u32")]
681 pub off_peak_energy_export: Option<u32>,
682
683 #[serde(deserialize_with = "opt_nan_u32")]
685 pub shoulder_energy_export: Option<u32>,
686
687 #[serde(deserialize_with = "opt_nan_u32")]
689 pub high_shoulder_energy_export: Option<u32>,
690}
691
692#[derive(Debug, Clone, Deserialize)]
696pub struct AggregatedOutput {
697 pub date: AggregateDate,
699
700 pub outputs: u32,
702
703 pub energy_generation: u64,
705
706 pub energy_efficiency: f32,
708
709 pub energy_exported: u64,
711
712 pub energy_used: u64,
714
715 #[serde(deserialize_with = "opt_nan_f32")]
717 pub peak_energy_import: Option<f32>,
718
719 #[serde(deserialize_with = "opt_nan_f32")]
721 pub off_peak_energy_import: Option<f32>,
722
723 #[serde(deserialize_with = "opt_nan_f32")]
725 pub shoulder_energy_import: Option<f32>,
726
727 #[serde(deserialize_with = "opt_nan_f32")]
729 pub high_shoulder_energy_import: Option<f32>,
730
731 #[serde(skip)]
732 pub export: Option<OutputExport>,
734}
735
736#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
740pub enum AggregateDate {
741 Year(u16),
743
744 YearAndMonth(u16, u8),
746}
747
748impl<'de> Deserialize<'de> for AggregateDate {
749 fn deserialize<D>(d: D) -> Result<Self, D::Error>
750 where
751 D: Deserializer<'de>,
752 {
753 let s = String::deserialize(d)?;
754 Ok(if s.len() == 4 {
755 Self::Year(s.parse().map_err(Error::custom)?)
756 } else {
757 let (year, month) = s.split_at_checked(4).ok_or_else(|| Error::custom(format!("invalid date: {s}")))?;
758 Self::YearAndMonth(year.parse().map_err(Error::custom)?, month.parse().map_err(Error::custom)?)
759 })
760 }
761}
762
763impl AggregateDate {
764 pub fn as_arbitrary_date(&self) -> Result<Date, jiff::Error> {
784 #[allow(clippy::cast_possible_wrap)]
785 match self {
786 Self::Year(year) => Date::new(*year as i16, 1, 1),
787 Self::YearAndMonth(year, month) => Date::new(*year as i16, *month as i8, 1),
788 }
789 }
790
791 #[allow(clippy::must_use_candidate)]
800 pub const fn year(&self) -> u16 {
801 match self {
802 Self::Year(year) | Self::YearAndMonth(year, _) => *year,
803 }
804 }
805}
806
807#[derive(Debug, Clone, Deserialize)]
811pub struct TeamOutput {
812 pub date: Date,
814
815 pub outputs: u32,
817
818 pub energy_efficiency: f32,
820
821 pub energy_generation: u32,
823
824 pub average_energy_generation: u32,
826
827 pub energy_exported: u32,
829
830 pub energy_consumed: u32,
832
833 pub average_energy_consumption: u32,
835
836 pub energy_imported: u32,
838}
839
840#[derive(Debug, Clone, Deserialize)]
844pub struct ExtendedData {
845 pub date: Date,
847
848 #[serde(default, deserialize_with = "opt_nan_f32")]
850 pub extended_value_1: Option<f32>,
851
852 #[serde(default, deserialize_with = "opt_nan_f32")]
854 pub extended_value_2: Option<f32>,
855
856 #[serde(default, deserialize_with = "opt_nan_f32")]
858 pub extended_value_3: Option<f32>,
859
860 #[serde(default, deserialize_with = "opt_nan_f32")]
862 pub extended_value_4: Option<f32>,
863
864 #[serde(default, deserialize_with = "opt_nan_f32")]
866 pub extended_value_5: Option<f32>,
867
868 #[serde(default, deserialize_with = "opt_nan_f32")]
870 pub extended_value_6: Option<f32>,
871}
872
873#[derive(Debug, Clone, Deserialize, Default)]
877pub struct FavouriteSystem {
878 #[serde(skip)]
880 pub system: System,
881
882 pub id: u32,
884
885 pub status_interval: u32,
887}
888
889impl FavouriteSystem {
890 #[must_use]
891 pub(crate) const fn new(system: System, system_id: u32, status_interval: u32) -> Self {
892 Self {
893 system,
894 id: system_id,
895 status_interval,
896 }
897 }
898}
899
900#[derive(Debug, Clone, Deserialize)]
904pub struct Insolation {
905 pub time: Time,
907
908 pub power: u32,
910
911 pub energy: u32,
913}
914
915#[derive(Debug, Clone, Deserialize)]
919pub struct SearchResult {
920 pub name: String,
922
923 pub size: u64,
925
926 pub postcode: String,
928
929 pub orientation: Orientation,
931
932 pub outputs: u32,
934
935 #[serde(deserialize_with = "search_span")]
937 pub last_output: Option<Span>,
938
939 pub system_id: u32,
941
942 pub panel: Option<String>,
944
945 pub inverter: Option<String>,
947
948 #[serde(deserialize_with = "opt_nan_f32")]
950 pub distance: Option<f32>,
951
952 #[serde(deserialize_with = "opt_nan_f32")]
954 pub latitude: Option<f32>,
955
956 #[serde(deserialize_with = "opt_nan_f32")]
958 pub longitude: Option<f32>,
959}
960
961#[derive(Debug, Clone, Deserialize)]
965pub struct Team {
966 pub name: String,
968
969 pub size: u64,
971
972 pub average_size: u32,
974
975 pub systems: u32,
977
978 pub energy_generation: u64,
980
981 pub outputs: u32,
983
984 pub average_energy_generation: u32,
986
987 pub team_type: TeamType,
989
990 pub description: String,
992
993 pub created: Date,
995}
996
997#[derive(Debug, Clone, Deserialize)]
999pub enum TeamType {
1000 Panel,
1002
1003 Inverter,
1005
1006 Geographic,
1008
1009 Installer,
1011
1012 Software,
1014
1015 General,
1017}
1018
1019#[derive(Debug, Clone, Deserialize)]
1023pub struct Supply {
1024 pub timestamp: Timestamp,
1026
1027 pub region: Region,
1029
1030 pub utilisation: f32,
1032
1033 pub power_output: u64,
1035
1036 pub power_input: u64,
1038
1039 pub average_power_output: u32,
1041
1042 pub average_power_input: u32,
1044
1045 pub average_net_power: i32,
1047
1048 pub systems_out: u32,
1050
1051 pub systems_in: u32,
1053
1054 pub total_size: u64,
1056
1057 #[serde(deserialize_with = "opt_negative_u32")]
1059 pub average_size: Option<u32>,
1060}
1061
1062#[derive(Clone, Debug, Deserialize)]
1066pub struct BatchStatusReport {
1067 pub date: Date,
1069
1070 pub time: Time,
1072
1073 #[serde(deserialize_with = "bool_int")]
1075 pub success: bool,
1076}
1077
1078pub(crate) fn opt_nan_f32<'de, D>(d: D) -> Result<Option<f32>, D::Error>
1082where
1083 D: Deserializer<'de>,
1084{
1085 Option::<f32>::deserialize(d).map(|r| r.filter(|f| !f.is_nan()))
1086}
1087
1088pub(crate) fn opt_nan_u32<'de, D>(d: D) -> Result<Option<u32>, D::Error>
1090where
1091 D: Deserializer<'de>,
1092{
1093 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
1094 Option::<f32>::deserialize(d).map(|r| r.filter(|f| !f.is_nan()).map(|f| f as u32))
1095}
1096
1097pub(crate) fn opt_nan_u64<'de, D>(d: D) -> Result<Option<u64>, D::Error>
1099where
1100 D: Deserializer<'de>,
1101{
1102 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
1103 Option::<f64>::deserialize(d).map(|r| r.filter(|f| !f.is_nan()).map(|f| f as u64))
1104}
1105
1106pub(crate) fn opt_nan_time<'de, D>(d: D) -> Result<Option<Time>, D::Error>
1108where
1109 D: Deserializer<'de>,
1110{
1111 match String::deserialize(d)?.as_str() {
1112 "NaN" => Ok(None),
1113 s => Ok(Some(s.parse().map_err(Error::custom)?)),
1114 }
1115}
1116
1117pub(crate) fn opt_negative_u32<'de, D>(d: D) -> Result<Option<u32>, D::Error>
1119where
1120 D: Deserializer<'de>,
1121{
1122 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
1123 match i64::deserialize(d)? {
1124 -1 => Ok(None),
1125 n => Ok(Some(u32::try_from(n).map_err(Error::custom)?)),
1126 }
1127}
1128
1129fn nullable<'de, D, T>(d: D) -> Result<Option<T>, D::Error>
1131where
1132 D: Deserializer<'de>,
1133 T: DeserializeOwned,
1134{
1135 #[derive(Deserialize)]
1137 #[serde(untagged)]
1138 enum Maybe<T> {
1139 Some(Option<T>),
1140 OtherString(String),
1141 }
1142
1143 match Deserialize::deserialize(d)? {
1144 Maybe::Some(s) => Ok(s),
1145 Maybe::OtherString(s) if s == "null" => Ok(None),
1146 Maybe::OtherString(s) => Err(Error::custom(format!("invalid value: {s}"))),
1147 }
1148}
1149
1150fn search_span<'de, D>(d: D) -> Result<Option<Span>, D::Error>
1157where
1158 D: Deserializer<'de>,
1159{
1160 Ok(match String::deserialize(d)?.as_str() {
1161 "No Outputs" => None,
1162 "Today" => Some(Span::new()),
1163 "Yesterday" => Some(Span::new().days(1)),
1164 days if days.ends_with("days ago") => {
1165 Some(Span::new().days(days.split(' ').next().unwrap().parse::<i64>().map_err(Error::custom)?))
1166 }
1167 weeks if weeks.ends_with("weeks ago") || weeks.ends_with("week ago") => {
1168 Some(Span::new().weeks(weeks.split(' ').next().unwrap().parse::<i64>().map_err(Error::custom)?))
1169 }
1170 other => return Err(Error::custom(format!("Couldn't parse {other} as Span!"))),
1171 })
1172}
1173
1174fn bool_int<'de, D>(d: D) -> Result<bool, D::Error>
1176where
1177 D: Deserializer<'de>,
1178{
1179 Ok(match u8::deserialize(d)? {
1180 0 => false,
1181 1 => true,
1182 _ => return Err(Error::custom("invalid value")),
1183 })
1184}