pvoutput_client/types/
response.rs

1//! API response types.
2
3use 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/// An API response along with current rate limit information.
16#[derive(Debug, Clone)]
17pub struct Response<T> {
18	/// Data returned by the API.
19	pub data: T,
20
21	/// Rate limit information.
22	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/// Rate limit information: the number of requests that can be made per hour, the remaining request count, and the time
30/// at which the limit will be reset.
31///
32/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#http-headers)
33#[derive(Debug, Clone)]
34pub struct RateLimit {
35	/// The total request limit for the hour.
36	pub limit: u16,
37
38	/// The remaining requests for the hour.
39	pub remaining: u16,
40
41	/// The time at which the rate limit will be reset.
42	pub reset: Timestamp,
43}
44
45impl RateLimit {
46	/// The number of requests that have been used in this hourly block.
47	///
48	/// # Example
49	/// ```rust
50	/// use pvoutput_client::types::response::RateLimit;
51	/// use jiff::Timestamp;
52	/// let rate_limit = RateLimit { limit: 300, remaining: 100, reset: Timestamp::now() };
53	/// assert_eq!(rate_limit.used(), 200);
54	/// ```
55	#[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/// Information returned by the [`get_status`](crate::Client::get_status) endpoint.
89#[derive(Debug, Clone, Deserialize)]
90pub struct Status {
91	/// The date of the status.
92	pub date: Date,
93
94	/// The time of the status.
95	pub time: Time,
96
97	/// Energy generation in watt-hours.
98	pub energy_generation: f32,
99
100	/// Power generation in watts.
101	pub power_generation: f32,
102
103	/// Energy consumption in watt-hours.
104	#[serde(deserialize_with = "opt_nan_f32")]
105	pub energy_consumption: Option<f32>,
106
107	/// Power consumption in watts.
108	#[serde(deserialize_with = "opt_nan_f32")]
109	pub power_consumption: Option<f32>,
110
111	/// Normalised output in kilowatts per kilowatt.
112	// TODO: what does that even mean?
113	pub normalised_output: f32,
114
115	/// Temperature in degrees Celsius.
116	#[serde(deserialize_with = "opt_nan_f32")]
117	pub temperature: Option<f32>,
118
119	/// Voltage in volts.
120	#[serde(deserialize_with = "opt_nan_f32")]
121	pub voltage: Option<f32>,
122
123	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 1.
124	#[serde(default)]
125	pub extended_value_1: Option<f32>,
126
127	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 2.
128	#[serde(default)]
129	pub extended_value_2: Option<f32>,
130
131	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 3.
132	#[serde(default)]
133	pub extended_value_3: Option<f32>,
134
135	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 4.
136	#[serde(default)]
137	pub extended_value_4: Option<f32>,
138
139	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 5.
140	#[serde(default)]
141	pub extended_value_5: Option<f32>,
142
143	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 6.
144	#[serde(default)]
145	pub extended_value_6: Option<f32>,
146}
147
148/// Historic status.
149///
150/// Returned by the [`get_status_history`](crate::Client::get_status_history) endpoint.
151#[derive(Debug, Clone, Deserialize)]
152pub struct History {
153	/// The date of the status.
154	pub date: Date,
155
156	/// The time of the status.
157	pub time: Time,
158
159	/// Energy generation in watt-hours.
160	pub energy_generation: f32,
161
162	/// Energy efficiency in kilowatt-hours per kilowatt-hour.
163	pub energy_efficiency: f32,
164
165	/// Instantaneous power generation in watts.
166	pub instantaneous_power: f32,
167
168	/// Average power generation in watts.
169	pub average_power: f32,
170
171	/// Normalised output in kilowatts per kilowatt.
172	// TODO: what does that even mean?
173	pub normalised_output: f32,
174
175	/// Energy consumption in watt-hours.
176	#[serde(deserialize_with = "opt_nan_f32")]
177	pub energy_consumption: Option<f32>,
178
179	/// Power consumption in watts.
180	#[serde(deserialize_with = "opt_nan_f32")]
181	pub power_consumption: Option<f32>,
182
183	/// Temperature in degrees Celsius.
184	#[serde(deserialize_with = "opt_nan_f32")]
185	pub temperature: Option<f32>,
186
187	/// Voltage in volts.
188	#[serde(deserialize_with = "opt_nan_f32")]
189	pub voltage: Option<f32>,
190
191	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 1.
192	#[serde(default)]
193	pub extended_value_1: Option<f32>,
194
195	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 2.
196	#[serde(default)]
197	pub extended_value_2: Option<f32>,
198
199	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 3.
200	#[serde(default)]
201	pub extended_value_3: Option<f32>,
202
203	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 4.
204	#[serde(default)]
205	pub extended_value_4: Option<f32>,
206
207	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 5.
208	#[serde(default)]
209	pub extended_value_5: Option<f32>,
210
211	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 6.
212	#[serde(default)]
213	pub extended_value_6: Option<f32>,
214}
215
216/// Daily status summary.
217///
218/// Returned by the [`get_status_daily`](crate::Client::get_status_daily) endpoint.
219#[derive(Debug, Clone, Deserialize)]
220pub struct DailyStatus {
221	/// Energy generation in watt-hours.
222	pub energy_generation: f32,
223
224	/// Power generation in watts.
225	pub power_generation: f32,
226
227	/// Peak power generation in watts.
228	pub peak_power_generation: f32,
229
230	/// Time at which the peak power generation occurred.
231	pub peak_time: Time,
232
233	/// Energy consumption data.
234	#[serde(skip)]
235	pub consumption: Option<DailyConsumption>,
236
237	/// Temperature data.
238	#[serde(skip)]
239	pub temperatures: Option<DailyTemperatures>,
240}
241
242/// Daily power and energy consumption data.
243///
244/// Part of a [`DailyStatus`] response.
245#[derive(Debug, Clone, Deserialize, Default)]
246pub struct DailyConsumption {
247	/// Energy consumption in watt-hours.
248	pub energy_consumption: Option<f32>,
249
250	/// Power consumption in watts.
251	pub power_consumption: Option<f32>,
252
253	/// Standby power consumption(?)
254	pub standby_power_consumption: Option<f32>,
255
256	/// Standby power time(?)
257	pub standby_power_time: Option<Time>,
258}
259
260/// Daily temperature data.
261///
262/// Part of a [`DailyStatus`] response.
263#[derive(Debug, Clone, Deserialize, Default)]
264pub struct DailyTemperatures {
265	/// Lowest recorded temperature in degrees Celsius.
266	pub lowest_temperature: Option<f32>,
267
268	/// Highest recorded temperature in degrees Celsius.
269	pub highest_temperature: Option<f32>,
270
271	/// Average temperature in degrees Celsius.
272	pub average_temperature: Option<f32>,
273}
274
275/// System statistics.
276///
277/// Returned by the [`get_statistics`](crate::Client::get_statistics) endpoint.
278#[derive(Debug, Clone, Deserialize)]
279pub struct Statistics {
280	/// Energy generated in watt-hours.
281	pub energy_generated: u64,
282
283	/// Energy exported in watt-hours.
284	pub energy_exported: u64,
285
286	/// Average energy generation in watt-hours.
287	pub average_energy_generation: u32,
288
289	/// Minimum energy generation in watt-hours.
290	pub minimum_energy_generation: u32,
291
292	/// Maximum energy generation in watt-hours.
293	pub maximum_energy_generation: u32,
294
295	/// Average efficiency in kilowatt-hours per kilowatt-hour.
296	pub average_efficiency: f32,
297
298	/// Number of outputs recorded.
299	pub outputs: u32,
300
301	/// Date from which these data were measured.
302	pub date_from: Date,
303
304	/// Date to which these data were measured.
305	pub date_to: Date,
306
307	/// The best daily energy efficiency in kilowatt-hours per kilowatt-hour.
308	pub record_efficiency: f32,
309
310	/// The date of the best daily energy efficiency.
311	pub record_date: Date,
312
313	/// Energy consumption data.
314	#[serde(skip)]
315	pub consumption: Option<ConsumptionStatistics>,
316
317	/// Credit amount.
318	#[serde(skip)]
319	pub credit: Option<f32>,
320
321	/// Debit amount.
322	#[serde(skip)]
323	pub debit: Option<f32>,
324}
325
326/// Consumption statistics.
327/// All figures are in watt-hours.
328///
329/// Part of a [`Statistics`] response.
330#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
331pub struct ConsumptionStatistics {
332	/// Energy consumed.
333	pub energy_consumed: u64,
334
335	/// Peak energy import.
336	pub peak_energy_import: u32,
337
338	/// Off-peak energy import.
339	pub off_peak_energy_import: u32,
340
341	/// Shoulder energy import.
342	pub shoulder_energy_import: u32,
343
344	/// High-shoulder energy import.
345	pub high_shoulder_energy_import: u32,
346
347	/// Average consumption.
348	pub average_consumption: u32,
349
350	/// Minimum consumption.
351	pub minimum_consumption: u32,
352
353	/// Maximum consumption.
354	pub maximum_consumption: u32,
355}
356
357/// Common system information.
358///
359/// Returned by the [`get_system`](crate::Client::get_system) and
360/// [`get_favourites`](crate::Client::get_favourites) endpoints.
361#[derive(Debug, Clone, Deserialize, Default)]
362#[serde(default)]
363pub struct System {
364	/// System name.
365	pub name: String,
366
367	/// System size in watts.
368	pub size: u64,
369
370	/// Postcode or Zipcode.
371	pub postcode: Option<String>,
372
373	/// Number of panels in the primary array.
374	pub panels: Option<u32>,
375
376	/// Power of each panel in the primary array in watts.
377	pub panel_power: Option<u32>,
378
379	/// Panel brand.
380	pub brand: Option<String>,
381
382	/// Number of inverters.
383	pub inverters: Option<u32>,
384
385	/// Inverter power in watts.
386	pub inverter_power: Option<u64>,
387
388	/// Inverter brand.
389	pub inverter_brand: Option<String>,
390
391	/// Orientation of the primary solar array.
392	pub orientation: Option<Orientation>,
393
394	/// Tilt of the primary solar array.
395	#[serde(deserialize_with = "opt_nan_f32")]
396	pub tilt: Option<f32>,
397
398	/// System shade level.
399	#[serde(deserialize_with = "nullable")]
400	pub shade: Option<Shade>,
401
402	/// System installation date.
403	pub install_date: Option<Date>,
404
405	/// System location latitude.
406	#[serde(deserialize_with = "opt_nan_f32")]
407	pub latitude: Option<f32>,
408
409	/// System location longitude.
410	#[serde(deserialize_with = "opt_nan_f32")]
411	pub longitude: Option<f32>,
412}
413
414/// Full system information.
415///
416/// Returned by the [`get_system`](crate::Client::get_system) endpoint.
417#[derive(Debug, Clone, Deserialize, Default)]
418pub struct FullSystem {
419	/// Base system information.
420	#[serde(skip)]
421	pub system: System,
422
423	/// Status update frequency in minutes.
424	pub status_interval: Option<u32>,
425
426	/// Number of panels in the secondary array.
427	#[serde(default)]
428	pub secondary_panels: Option<u32>,
429
430	/// Power of each panel in the secondary array in watts.
431	#[serde(default)]
432	pub secondary_panel_power: Option<u32>,
433
434	/// Orientation of the secondary solar array.
435	#[serde(deserialize_with = "nullable", default)]
436	pub secondary_orientation: Option<Orientation>,
437
438	/// Tilt of the secondary solar array.
439	#[serde(deserialize_with = "opt_nan_f32", default)]
440	pub secondary_tilt: Option<f32>,
441
442	/// Tariff information.
443	#[serde(skip)]
444	pub tariffs: Tariffs,
445
446	/// Participating teams.
447	#[serde(skip)]
448	pub teams: Vec<u32>,
449
450	/// Number of donations.
451	#[serde(skip)]
452	pub donations: u32,
453
454	/// Extended data.
455	#[serde(skip)]
456	pub extended_data: Vec<Option<f32>>,
457
458	/// Monthly estimates.
459	#[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/// System [tariff](https://www.energy.gov.au/solar/financial-benefits-solar/electricity-pricing-plans-and-tariffs)
473/// information.
474///
475/// Part of a [`FullSystem`] response.
476#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq)]
477#[serde(default)]
478pub struct Tariffs {
479	/// Export tariff in cents.
480	#[serde(deserialize_with = "opt_nan_f32")]
481	pub export: Option<f32>,
482
483	/// Import peak tariff in cents.
484	#[serde(deserialize_with = "opt_nan_f32")]
485	pub import_peak: Option<f32>,
486
487	/// Import off-peak tariff in cents.
488	#[serde(deserialize_with = "opt_nan_f32")]
489	pub import_off_peak: Option<f32>,
490
491	/// Import shoulder tariff in cents.
492	#[serde(deserialize_with = "opt_nan_f32")]
493	pub import_shoulder: Option<f32>,
494
495	/// Import high shoulder tariff in cents.
496	#[serde(deserialize_with = "opt_nan_f32")]
497	pub import_high_shoulder: Option<f32>,
498
499	/// Daily import charge in cents.
500	#[serde(deserialize_with = "opt_nan_f32")]
501	pub daily: Option<f32>,
502}
503
504/// Solar array panel orientation.
505#[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/// Solar array shade level.
536#[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq, Hash, PartialOrd, Ord)]
537pub enum Shade {
538	/// No shade.
539	No,
540
541	/// Low shade.
542	Low,
543
544	/// Medium shade.
545	Medium,
546
547	/// High shade.
548	High,
549}
550
551/// Ladder ranking information.
552///
553/// Returned by the [`get_ladder`](crate::Client::get_ladder) endpoint.
554#[derive(Debug, Clone, Deserialize)]
555pub struct Ladder {
556	/// Ranking date.
557	pub date: Date,
558
559	/// Generation rank.
560	#[serde(deserialize_with = "opt_nan_u32")]
561	pub generation_rank: Option<u32>,
562
563	/// Efficiency rank.
564	#[serde(deserialize_with = "opt_nan_u32")]
565	pub efficiency_rank: Option<u32>,
566
567	/// Average efficiency in kilowatt-hours per kilowatt-hour.
568	#[serde(deserialize_with = "opt_nan_f32")]
569	pub efficiency: Option<f32>,
570
571	/// Total number of outputs in days.
572	#[serde(deserialize_with = "opt_nan_u32")]
573	pub total_outputs: Option<u32>,
574
575	/// Date of the latest output considered by the ranking.
576	pub last_output: Option<Date>,
577
578	/// Total generation in watt-hours.
579	#[serde(deserialize_with = "opt_nan_u64")]
580	pub total_generation: Option<u64>,
581
582	/// Total consumption in watt-hours.
583	#[serde(deserialize_with = "opt_nan_u64")]
584	pub total_consumption: Option<u64>,
585
586	/// Maximum generation in watt-hours.
587	#[serde(deserialize_with = "opt_nan_u32")]
588	pub max_generation: Option<u32>,
589
590	/// Maximum consumption in watt-hours.
591	#[serde(deserialize_with = "opt_nan_u32")]
592	pub max_consumption: Option<u32>,
593
594	/// Age of this system in days.
595	#[serde(deserialize_with = "opt_nan_u32")]
596	pub system_age: Option<u32>,
597}
598
599impl Ladder {
600	/// Returns a URL for this system's generation rank on PVOutput.org's `ladder.jsp` page.
601	#[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/// A daily output.
610///
611/// Returned by the [`get_outputs`](crate::Client::get_outputs) endpoint.
612#[derive(Debug, Clone, Deserialize)]
613pub struct Output {
614	pub date: Date,
615
616	/// Energy generated in watt-hours.
617	pub energy_generation: u32,
618
619	/// Energy efficiency in kilowatt-hours per kilowatt-hour.
620	pub energy_efficiency: f32,
621
622	/// Energy exported in watt-hours.
623	pub energy_exported: u32,
624
625	/// Energy used in watt-hours.
626	pub energy_used: u32,
627
628	/// Peak power generation in watts.
629	#[serde(deserialize_with = "opt_nan_u32")]
630	pub peak_power: Option<u32>,
631
632	/// Peak power generation time.
633	#[serde(deserialize_with = "opt_nan_time")]
634	pub peak_time: Option<Time>,
635
636	/// Weather conditions.
637	pub conditions: Conditions,
638
639	/// Minimum temperature in degrees Celsius.
640	#[serde(deserialize_with = "opt_nan_u32")]
641	pub min_temperature: Option<u32>,
642
643	/// Maximum temperature in degrees Celsius.
644	#[serde(deserialize_with = "opt_nan_u32")]
645	pub max_temperature: Option<u32>,
646
647	/// Peak energy import in watt-hours.
648	#[serde(deserialize_with = "opt_nan_u32")]
649	pub peak_energy_import: Option<u32>,
650
651	/// Off-peak energy import in watt-hours.
652	#[serde(deserialize_with = "opt_nan_u32")]
653	pub off_peak_energy_import: Option<u32>,
654
655	/// Shoulder energy import in watt-hours.
656	#[serde(deserialize_with = "opt_nan_u32")]
657	pub shoulder_energy_import: Option<u32>,
658
659	/// High-shoulder energy import in watt-hours.
660	#[serde(deserialize_with = "opt_nan_u32")]
661	pub high_shoulder_energy_import: Option<u32>,
662
663	#[serde(skip)]
664	/// Export data.
665	pub export: Option<OutputExport>,
666
667	/// Insolation in watt-hours.
668	#[serde(skip)]
669	pub insolation: Option<u32>,
670}
671
672/// Energy export information for an [`Output`].
673#[derive(Clone, Debug, Deserialize, Default)]
674pub struct OutputExport {
675	/// Peak energy export in watt-hours.
676	#[serde(deserialize_with = "opt_nan_u32")]
677	pub peak_energy_export: Option<u32>,
678
679	/// Off-peak energy export in watt-hours.
680	#[serde(deserialize_with = "opt_nan_u32")]
681	pub off_peak_energy_export: Option<u32>,
682
683	/// Shoulder energy export in watt-hours.
684	#[serde(deserialize_with = "opt_nan_u32")]
685	pub shoulder_energy_export: Option<u32>,
686
687	/// High-shoulder energy export in watt-hours.
688	#[serde(deserialize_with = "opt_nan_u32")]
689	pub high_shoulder_energy_export: Option<u32>,
690}
691
692/// Aggregated output data.
693///
694/// Returned by the [`get_aggregate_outputs`](crate::Client::get_aggregate_outputs) endpoint.
695#[derive(Debug, Clone, Deserialize)]
696pub struct AggregatedOutput {
697	/// The month or year of the aggregated output.
698	pub date: AggregateDate,
699
700	/// Number of outputs.
701	pub outputs: u32,
702
703	/// Energy generated in watt-hours.
704	pub energy_generation: u64,
705
706	/// Energy efficiency in kilowatt-hours per kilowatt-hour.
707	pub energy_efficiency: f32,
708
709	/// Energy exported in watt-hours.
710	pub energy_exported: u64,
711
712	/// Energy used in watt-hours.
713	pub energy_used: u64,
714
715	/// Peak energy import in watt-hours.
716	#[serde(deserialize_with = "opt_nan_f32")]
717	pub peak_energy_import: Option<f32>,
718
719	/// Off-peak energy import in watt-hours.
720	#[serde(deserialize_with = "opt_nan_f32")]
721	pub off_peak_energy_import: Option<f32>,
722
723	/// Shoulder energy import in watt-hours.
724	#[serde(deserialize_with = "opt_nan_f32")]
725	pub shoulder_energy_import: Option<f32>,
726
727	/// High-shoulder energy import in watt-hours.
728	#[serde(deserialize_with = "opt_nan_f32")]
729	pub high_shoulder_energy_import: Option<f32>,
730
731	#[serde(skip)]
732	/// Export data.
733	pub export: Option<OutputExport>,
734}
735
736/// A date range for an [`AggregatedOutput`].
737///
738/// Represents either a year or a year and month.
739#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
740pub enum AggregateDate {
741	/// A year.
742	Year(u16),
743
744	/// A year and month.
745	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	/// Converts the [`AggregateDate`] to an arbitrary jiff [`Date`].
765	/// - A `Year` will be converted to a `Date` on the first day of the first month of that year.
766	/// - A `YearAndMonth` will be converted to a `Date` on the first day of that month of that year.
767	///
768	/// # Errors
769	/// Will return an error if the [`AggregateDate`] is not a valid, jiff-expressible year or year and month.
770	/// For example, `Year(10_000)` will return an error, as years in jiff must be below 9999.
771	///
772	/// # Example
773	/// ```rust
774	/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
775	/// use pvoutput_client::types::response::AggregateDate;
776	/// use jiff::civil::Date;
777	///
778	/// assert_eq!(AggregateDate::Year(2025).as_arbitrary_date()?, Date::constant(2025, 1, 1));
779	/// assert_eq!(AggregateDate::YearAndMonth(2025, 9).as_arbitrary_date()?, Date::constant(2025, 9, 1));
780	/// # Ok(())
781	/// # }
782	/// ```
783	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	/// Returns this [`AggregateDate`]'s year.
792	///
793	/// # Example
794	/// ```rust
795	/// use pvoutput_client::types::response::AggregateDate;
796	/// assert_eq!(AggregateDate::Year(2025).year(), 2025);
797	/// assert_eq!(AggregateDate::YearAndMonth(2025, 9).year(), 2025);
798	/// ```
799	#[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/// Daily team output data.
808///
809/// Returned by the [`get_team_outputs`](crate::Client::get_team_outputs) endpoint.
810#[derive(Debug, Clone, Deserialize)]
811pub struct TeamOutput {
812	/// The date this output corresponds to.
813	pub date: Date,
814
815	/// Number of outputs.
816	pub outputs: u32,
817
818	/// Energy efficiency in kilowatt-hours per kilowatt-hour.
819	pub energy_efficiency: f32,
820
821	/// Total energy generated in watt-hours.
822	pub energy_generation: u32,
823
824	/// Average energy generation in watt-hours.
825	pub average_energy_generation: u32,
826
827	/// Total energy exported in watt-hours.
828	pub energy_exported: u32,
829
830	/// Total energy consumed in watt-hours.
831	pub energy_consumed: u32,
832
833	/// Average energy consumption in watt-hours.
834	pub average_energy_consumption: u32,
835
836	/// Total energy imported in watt-hours.
837	pub energy_imported: u32,
838}
839
840/// One day of user-defined [extended data](https://pvoutput.org/help/extended_data.html).
841///
842/// Returned by the [`get_extended`](crate::Client::get_extended) endpoint.
843#[derive(Debug, Clone, Deserialize)]
844pub struct ExtendedData {
845	/// The date for these data.
846	pub date: Date,
847
848	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 1.
849	#[serde(default, deserialize_with = "opt_nan_f32")]
850	pub extended_value_1: Option<f32>,
851
852	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 2.
853	#[serde(default, deserialize_with = "opt_nan_f32")]
854	pub extended_value_2: Option<f32>,
855
856	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 3.
857	#[serde(default, deserialize_with = "opt_nan_f32")]
858	pub extended_value_3: Option<f32>,
859
860	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 4.
861	#[serde(default, deserialize_with = "opt_nan_f32")]
862	pub extended_value_4: Option<f32>,
863
864	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 5.
865	#[serde(default, deserialize_with = "opt_nan_f32")]
866	pub extended_value_5: Option<f32>,
867
868	/// User-defined [extended data](https://pvoutput.org/help/extended_data.html) parameter 6.
869	#[serde(default, deserialize_with = "opt_nan_f32")]
870	pub extended_value_6: Option<f32>,
871}
872
873/// A favourite system.
874///
875/// Returned by the [`get_favourites`](crate::Client::get_favourites) endpoint.
876#[derive(Debug, Clone, Deserialize, Default)]
877pub struct FavouriteSystem {
878	/// System information.
879	#[serde(skip)]
880	pub system: System,
881
882	/// System ID.
883	pub id: u32,
884
885	/// Status interval in minutes.
886	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/// Insolation data.
901///
902/// Returned by the [`get_insolation`](crate::Client::get_insolation) endpoint.
903#[derive(Debug, Clone, Deserialize)]
904pub struct Insolation {
905	/// Time for the calculated insolation data.
906	pub time: Time,
907
908	/// Power in watts.
909	pub power: u32,
910
911	/// Energy in watt-hours.
912	pub energy: u32,
913}
914
915/// A search result.
916///
917/// Returned by the [`search`](crate::Client::search) endpoint.
918#[derive(Debug, Clone, Deserialize)]
919pub struct SearchResult {
920	/// Name.
921	pub name: String,
922
923	/// Size in watts.
924	pub size: u64,
925
926	/// Postcode.
927	pub postcode: String,
928
929	/// Orientation.
930	pub orientation: Orientation,
931
932	/// Number of outputs.
933	pub outputs: u32,
934
935	/// How long ago the last output was recorded, or `None` if the system has no outputs.
936	#[serde(deserialize_with = "search_span")]
937	pub last_output: Option<Span>,
938
939	/// System ID.
940	pub system_id: u32,
941
942	/// Panel brand.
943	pub panel: Option<String>,
944
945	/// Inverter brand.
946	pub inverter: Option<String>,
947
948	/// Distance in kilometres from the location provided in the search query.
949	#[serde(deserialize_with = "opt_nan_f32")]
950	pub distance: Option<f32>,
951
952	/// Latitude.
953	#[serde(deserialize_with = "opt_nan_f32")]
954	pub latitude: Option<f32>,
955
956	/// Longitude.
957	#[serde(deserialize_with = "opt_nan_f32")]
958	pub longitude: Option<f32>,
959}
960
961/// A [team](https://pvoutput.org/help/teams.html).
962///
963/// Returned by the [`get_team`](crate::Client::get_team) endpoint.
964#[derive(Debug, Clone, Deserialize)]
965pub struct Team {
966	/// Team name.
967	pub name: String,
968
969	/// Team system size in watts.
970	pub size: u64,
971
972	/// Average system size in watts.
973	pub average_size: u32,
974
975	/// Number of systems in this team.
976	pub systems: u32,
977
978	/// Total energy generated in watt-hours.
979	pub energy_generation: u64,
980
981	/// Number of outputs recorded.
982	pub outputs: u32,
983
984	/// Average energy generation in watt-hours.
985	pub average_energy_generation: u32,
986
987	/// Type of team.
988	pub team_type: TeamType,
989
990	/// Team description.
991	pub description: String,
992
993	/// Team creation date.
994	pub created: Date,
995}
996
997/// A category for a [`Team`].
998#[derive(Debug, Clone, Deserialize)]
999pub enum TeamType {
1000	/// A team for a particular panel brand.
1001	Panel,
1002
1003	/// A team for a particular inverter brand.
1004	Inverter,
1005
1006	/// A team for a geographic region or location.
1007	Geographic,
1008
1009	/// A team for a particular installer.
1010	Installer,
1011
1012	/// A team for a specific piece of software.
1013	Software,
1014
1015	/// A general team.
1016	General,
1017}
1018
1019/// Supply and demand information for a particular region.
1020///
1021/// Returned by the [`get_supply`](crate::Client::get_supply) endpoint.
1022#[derive(Debug, Clone, Deserialize)]
1023pub struct Supply {
1024	/// Timestamp for the aggregated data.
1025	pub timestamp: Timestamp,
1026
1027	/// The region these data are for.
1028	pub region: Region,
1029
1030	/// Utilisation as a percentage.
1031	pub utilisation: f32,
1032
1033	/// Total power output in watts.
1034	pub power_output: u64,
1035
1036	/// Total power input in watts.
1037	pub power_input: u64,
1038
1039	/// Average power output in watts.
1040	pub average_power_output: u32,
1041
1042	/// Average power input in watts.
1043	pub average_power_input: u32,
1044
1045	/// Average net power in watts.
1046	pub average_net_power: i32,
1047
1048	/// Systems outputting power(?).
1049	pub systems_out: u32,
1050
1051	/// Systems consuming power(?).
1052	pub systems_in: u32,
1053
1054	/// Total size of this region's systems in watts.
1055	pub total_size: u64,
1056
1057	/// Average size of this region's systems in watts.
1058	#[serde(deserialize_with = "opt_negative_u32")]
1059	pub average_size: Option<u32>,
1060}
1061
1062/// Status information for a status uploaded to a batch status endpoint.
1063///
1064/// Returned by the [`crate::Client::add_batch_status`] and [`crate::Client::add_batch_status_net`] endpoints.
1065#[derive(Clone, Debug, Deserialize)]
1066pub struct BatchStatusReport {
1067	/// The date for this status.
1068	pub date: Date,
1069
1070	/// The time for this status.
1071	pub time: Time,
1072
1073	/// Whether this status was successful.
1074	#[serde(deserialize_with = "bool_int")]
1075	pub success: bool,
1076}
1077
1078// custom deserialiser functions
1079
1080/// Deserialises an `f32`, treating `NaN` as `None`.
1081pub(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
1088/// Deserialises a `u32`, treating `NaN` as `None`.
1089pub(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
1097/// Deserialises a `u64`, treating `NaN` as `None`.
1098pub(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
1106/// Deserialises a `Time`, treating `NaN` as `None`.
1107pub(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
1117/// Deserialises a `u32`, treating `-1` as `None`.
1118pub(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
1129/// Deserialises a nullable value, treating `null` as `None`.
1130fn nullable<'de, D, T>(d: D) -> Result<Option<T>, D::Error>
1131where
1132	D: Deserializer<'de>,
1133	T: DeserializeOwned,
1134{
1135	// h/t https://stackoverflow.com/a/56384732
1136	#[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
1150/// Parses the following:
1151/// - No Output -> `None`
1152/// - Today -> `Span::new()`
1153/// - Yesterday -> `Span::new().days(1)`
1154/// - n days ago -> `Span::new().days(n)`
1155/// - n weeks ago -> `Span::new().weeks(n)`
1156fn 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
1174/// Deserialises a boolean from an integer.
1175fn 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}