use jiff::{
Span, Timestamp,
civil::{Date, Time},
};
use reqwest::header::HeaderMap;
use serde::{
Deserialize, Deserializer,
de::{DeserializeOwned, Error},
};
use crate::types::{Conditions, error::RateLimitError, regions::Region};
#[derive(Debug, Clone)]
pub struct Response<T> {
pub data: T,
pub rate_limit: RateLimit,
}
impl<T> Response<T> {
pub(crate) const fn new(data: T, rate_limit: RateLimit) -> Self { Self { data, rate_limit } }
}
#[derive(Debug, Clone)]
pub struct RateLimit {
pub limit: u16,
pub remaining: u16,
pub reset: Timestamp,
}
impl RateLimit {
#[must_use]
pub const fn used(&self) -> u16 {
debug_assert!(self.remaining <= self.limit);
self.limit - self.remaining
}
}
impl TryFrom<&HeaderMap> for RateLimit {
type Error = RateLimitError;
fn try_from(value: &HeaderMap) -> Result<Self, Self::Error> {
if let (Some(limit), Some(remaining), Some(reset)) = (
value.get("X-Rate-Limit-Limit"),
value.get("X-Rate-Limit-Remaining"),
value.get("X-Rate-Limit-Reset"),
) {
let limit = limit.to_str()?.parse()?;
let remaining = remaining.to_str()?.parse()?;
let reset = Timestamp::from_second(reset.to_str()?.parse()?)?;
debug_assert!(remaining <= limit);
Ok(Self {
limit,
remaining,
reset,
})
} else {
Err(RateLimitError::MissingValues)
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Status {
pub date: Date,
pub time: Time,
pub energy_generation: f32,
pub power_generation: f32,
#[serde(deserialize_with = "opt_nan_f32")]
pub energy_consumption: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub power_consumption: Option<f32>,
pub normalised_output: f32,
#[serde(deserialize_with = "opt_nan_f32")]
pub temperature: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub voltage: Option<f32>,
#[serde(default)]
pub extended_value_1: Option<f32>,
#[serde(default)]
pub extended_value_2: Option<f32>,
#[serde(default)]
pub extended_value_3: Option<f32>,
#[serde(default)]
pub extended_value_4: Option<f32>,
#[serde(default)]
pub extended_value_5: Option<f32>,
#[serde(default)]
pub extended_value_6: Option<f32>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct History {
pub date: Date,
pub time: Time,
pub energy_generation: f32,
pub energy_efficiency: f32,
pub instantaneous_power: f32,
pub average_power: f32,
pub normalised_output: f32,
#[serde(deserialize_with = "opt_nan_f32")]
pub energy_consumption: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub power_consumption: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub temperature: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub voltage: Option<f32>,
#[serde(default)]
pub extended_value_1: Option<f32>,
#[serde(default)]
pub extended_value_2: Option<f32>,
#[serde(default)]
pub extended_value_3: Option<f32>,
#[serde(default)]
pub extended_value_4: Option<f32>,
#[serde(default)]
pub extended_value_5: Option<f32>,
#[serde(default)]
pub extended_value_6: Option<f32>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DailyStatus {
pub energy_generation: f32,
pub power_generation: f32,
pub peak_power_generation: f32,
pub peak_time: Time,
#[serde(skip)]
pub consumption: Option<DailyConsumption>,
#[serde(skip)]
pub temperatures: Option<DailyTemperatures>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct DailyConsumption {
pub energy_consumption: Option<f32>,
pub power_consumption: Option<f32>,
pub standby_power_consumption: Option<f32>,
pub standby_power_time: Option<Time>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct DailyTemperatures {
pub lowest_temperature: Option<f32>,
pub highest_temperature: Option<f32>,
pub average_temperature: Option<f32>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Statistics {
pub energy_generated: u64,
pub energy_exported: u64,
pub average_energy_generation: u32,
pub minimum_energy_generation: u32,
pub maximum_energy_generation: u32,
pub average_efficiency: f32,
pub outputs: u32,
pub date_from: Date,
pub date_to: Date,
pub record_efficiency: f32,
pub record_date: Date,
#[serde(skip)]
pub consumption: Option<ConsumptionStatistics>,
#[serde(skip)]
pub credit: Option<f32>,
#[serde(skip)]
pub debit: Option<f32>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct ConsumptionStatistics {
pub energy_consumed: u64,
pub peak_energy_import: u32,
pub off_peak_energy_import: u32,
pub shoulder_energy_import: u32,
pub high_shoulder_energy_import: u32,
pub average_consumption: u32,
pub minimum_consumption: u32,
pub maximum_consumption: u32,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
pub struct System {
pub name: String,
pub size: u64,
pub postcode: Option<String>,
pub panels: Option<u32>,
pub panel_power: Option<u32>,
pub brand: Option<String>,
pub inverters: Option<u32>,
pub inverter_power: Option<u64>,
pub inverter_brand: Option<String>,
pub orientation: Option<Orientation>,
#[serde(deserialize_with = "opt_nan_f32")]
pub tilt: Option<f32>,
#[serde(deserialize_with = "nullable")]
pub shade: Option<Shade>,
pub install_date: Option<Date>,
#[serde(deserialize_with = "opt_nan_f32")]
pub latitude: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub longitude: Option<f32>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct FullSystem {
#[serde(skip)]
pub system: System,
pub status_interval: Option<u32>,
#[serde(default)]
pub secondary_panels: Option<u32>,
#[serde(default)]
pub secondary_panel_power: Option<u32>,
#[serde(deserialize_with = "nullable", default)]
pub secondary_orientation: Option<Orientation>,
#[serde(deserialize_with = "opt_nan_f32", default)]
pub secondary_tilt: Option<f32>,
#[serde(skip)]
pub tariffs: Tariffs,
#[serde(skip)]
pub teams: Vec<u32>,
#[serde(skip)]
pub donations: u32,
#[serde(skip)]
pub extended_data: Vec<Option<f32>>,
#[serde(skip)]
pub monthly_estimates: Vec<f32>,
}
impl From<System> for FullSystem {
fn from(value: System) -> Self {
Self {
system: value,
..Default::default()
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq)]
#[serde(default)]
pub struct Tariffs {
#[serde(deserialize_with = "opt_nan_f32")]
pub export: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub import_peak: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub import_off_peak: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub import_shoulder: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub import_high_shoulder: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub daily: Option<f32>,
}
#[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq, Hash)]
pub enum Orientation {
#[serde(rename = "N")]
North,
#[serde(rename = "NE")]
NorthEast,
#[serde(rename = "NW")]
NorthWest,
#[serde(rename = "W")]
West,
#[serde(rename = "E")]
East,
#[serde(rename = "EW")]
EastWest,
#[serde(rename = "S")]
South,
#[serde(rename = "SE")]
SouthEast,
#[serde(rename = "SW")]
SouthWest,
}
#[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub enum Shade {
No,
Low,
Medium,
High,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Ladder {
pub date: Date,
#[serde(deserialize_with = "opt_nan_u32")]
pub generation_rank: Option<u32>,
#[serde(deserialize_with = "opt_nan_u32")]
pub efficiency_rank: Option<u32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub efficiency: Option<f32>,
#[serde(deserialize_with = "opt_nan_u32")]
pub total_outputs: Option<u32>,
pub last_output: Option<Date>,
#[serde(deserialize_with = "opt_nan_u64")]
pub total_generation: Option<u64>,
#[serde(deserialize_with = "opt_nan_u64")]
pub total_consumption: Option<u64>,
#[serde(deserialize_with = "opt_nan_u32")]
pub max_generation: Option<u32>,
#[serde(deserialize_with = "opt_nan_u32")]
pub max_consumption: Option<u32>,
#[serde(deserialize_with = "opt_nan_u32")]
pub system_age: Option<u32>,
}
impl Ladder {
#[must_use]
pub fn generation_rank_url(&self) -> Option<String> {
self
.generation_rank
.map(|rank| format!("https://pvoutput.org/ladder.jsp?rank={rank}&o=e&d=desc&filter=0&p={}", rank / 30))
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Output {
pub date: Date,
pub energy_generation: u32,
pub energy_efficiency: f32,
pub energy_exported: u32,
pub energy_used: u32,
#[serde(deserialize_with = "opt_nan_u32")]
pub peak_power: Option<u32>,
#[serde(deserialize_with = "opt_nan_time")]
pub peak_time: Option<Time>,
pub conditions: Conditions,
#[serde(deserialize_with = "opt_nan_u32")]
pub min_temperature: Option<u32>,
#[serde(deserialize_with = "opt_nan_u32")]
pub max_temperature: Option<u32>,
#[serde(deserialize_with = "opt_nan_u32")]
pub peak_energy_import: Option<u32>,
#[serde(deserialize_with = "opt_nan_u32")]
pub off_peak_energy_import: Option<u32>,
#[serde(deserialize_with = "opt_nan_u32")]
pub shoulder_energy_import: Option<u32>,
#[serde(deserialize_with = "opt_nan_u32")]
pub high_shoulder_energy_import: Option<u32>,
#[serde(skip)]
pub export: Option<OutputExport>,
#[serde(skip)]
pub insolation: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Default)]
pub struct OutputExport {
#[serde(deserialize_with = "opt_nan_u32")]
pub peak_energy_export: Option<u32>,
#[serde(deserialize_with = "opt_nan_u32")]
pub off_peak_energy_export: Option<u32>,
#[serde(deserialize_with = "opt_nan_u32")]
pub shoulder_energy_export: Option<u32>,
#[serde(deserialize_with = "opt_nan_u32")]
pub high_shoulder_energy_export: Option<u32>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AggregatedOutput {
pub date: AggregateDate,
pub outputs: u32,
pub energy_generation: u64,
pub energy_efficiency: f32,
pub energy_exported: u64,
pub energy_used: u64,
#[serde(deserialize_with = "opt_nan_f32")]
pub peak_energy_import: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub off_peak_energy_import: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub shoulder_energy_import: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub high_shoulder_energy_import: Option<f32>,
#[serde(skip)]
pub export: Option<OutputExport>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum AggregateDate {
Year(u16),
YearAndMonth(u16, u8),
}
impl<'de> Deserialize<'de> for AggregateDate {
fn deserialize<D>(d: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(d)?;
Ok(if s.len() == 4 {
Self::Year(s.parse().map_err(Error::custom)?)
} else {
let (year, month) = s.split_at_checked(4).ok_or_else(|| Error::custom(format!("invalid date: {s}")))?;
Self::YearAndMonth(year.parse().map_err(Error::custom)?, month.parse().map_err(Error::custom)?)
})
}
}
impl AggregateDate {
pub fn as_arbitrary_date(&self) -> Result<Date, jiff::Error> {
#[allow(clippy::cast_possible_wrap)]
match self {
Self::Year(year) => Date::new(*year as i16, 1, 1),
Self::YearAndMonth(year, month) => Date::new(*year as i16, *month as i8, 1),
}
}
#[allow(clippy::must_use_candidate)]
pub const fn year(&self) -> u16 {
match self {
Self::Year(year) | Self::YearAndMonth(year, _) => *year,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct TeamOutput {
pub date: Date,
pub outputs: u32,
pub energy_efficiency: f32,
pub energy_generation: u32,
pub average_energy_generation: u32,
pub energy_exported: u32,
pub energy_consumed: u32,
pub average_energy_consumption: u32,
pub energy_imported: u32,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ExtendedData {
pub date: Date,
#[serde(default, deserialize_with = "opt_nan_f32")]
pub extended_value_1: Option<f32>,
#[serde(default, deserialize_with = "opt_nan_f32")]
pub extended_value_2: Option<f32>,
#[serde(default, deserialize_with = "opt_nan_f32")]
pub extended_value_3: Option<f32>,
#[serde(default, deserialize_with = "opt_nan_f32")]
pub extended_value_4: Option<f32>,
#[serde(default, deserialize_with = "opt_nan_f32")]
pub extended_value_5: Option<f32>,
#[serde(default, deserialize_with = "opt_nan_f32")]
pub extended_value_6: Option<f32>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct FavouriteSystem {
#[serde(skip)]
pub system: System,
pub id: u32,
pub status_interval: u32,
}
impl FavouriteSystem {
#[must_use]
pub(crate) const fn new(system: System, system_id: u32, status_interval: u32) -> Self {
Self {
system,
id: system_id,
status_interval,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Insolation {
pub time: Time,
pub power: u32,
pub energy: u32,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SearchResult {
pub name: String,
pub size: u64,
pub postcode: String,
pub orientation: Orientation,
pub outputs: u32,
#[serde(deserialize_with = "search_span")]
pub last_output: Option<Span>,
pub system_id: u32,
pub panel: Option<String>,
pub inverter: Option<String>,
#[serde(deserialize_with = "opt_nan_f32")]
pub distance: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub latitude: Option<f32>,
#[serde(deserialize_with = "opt_nan_f32")]
pub longitude: Option<f32>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Team {
pub name: String,
pub size: u64,
pub average_size: u32,
pub systems: u32,
pub energy_generation: u64,
pub outputs: u32,
pub average_energy_generation: u32,
pub team_type: TeamType,
pub description: String,
pub created: Date,
}
#[derive(Debug, Clone, Deserialize)]
pub enum TeamType {
Panel,
Inverter,
Geographic,
Installer,
Software,
General,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Supply {
pub timestamp: Timestamp,
pub region: Region,
pub utilisation: f32,
pub power_output: u64,
pub power_input: u64,
pub average_power_output: u32,
pub average_power_input: u32,
pub average_net_power: i32,
pub systems_out: u32,
pub systems_in: u32,
pub total_size: u64,
#[serde(deserialize_with = "opt_negative_u32")]
pub average_size: Option<u32>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct BatchStatusReport {
pub date: Date,
pub time: Time,
#[serde(deserialize_with = "bool_int")]
pub success: bool,
}
pub(crate) fn opt_nan_f32<'de, D>(d: D) -> Result<Option<f32>, D::Error>
where
D: Deserializer<'de>,
{
Option::<f32>::deserialize(d).map(|r| r.filter(|f| !f.is_nan()))
}
pub(crate) fn opt_nan_u32<'de, D>(d: D) -> Result<Option<u32>, D::Error>
where
D: Deserializer<'de>,
{
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
Option::<f32>::deserialize(d).map(|r| r.filter(|f| !f.is_nan()).map(|f| f as u32))
}
pub(crate) fn opt_nan_u64<'de, D>(d: D) -> Result<Option<u64>, D::Error>
where
D: Deserializer<'de>,
{
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
Option::<f64>::deserialize(d).map(|r| r.filter(|f| !f.is_nan()).map(|f| f as u64))
}
pub(crate) fn opt_nan_time<'de, D>(d: D) -> Result<Option<Time>, D::Error>
where
D: Deserializer<'de>,
{
match String::deserialize(d)?.as_str() {
"NaN" => Ok(None),
s => Ok(Some(s.parse().map_err(Error::custom)?)),
}
}
pub(crate) fn opt_negative_u32<'de, D>(d: D) -> Result<Option<u32>, D::Error>
where
D: Deserializer<'de>,
{
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
match i64::deserialize(d)? {
-1 => Ok(None),
n => Ok(Some(u32::try_from(n).map_err(Error::custom)?)),
}
}
fn nullable<'de, D, T>(d: D) -> Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: DeserializeOwned,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Maybe<T> {
Some(Option<T>),
OtherString(String),
}
match Deserialize::deserialize(d)? {
Maybe::Some(s) => Ok(s),
Maybe::OtherString(s) if s == "null" => Ok(None),
Maybe::OtherString(s) => Err(Error::custom(format!("invalid value: {s}"))),
}
}
fn search_span<'de, D>(d: D) -> Result<Option<Span>, D::Error>
where
D: Deserializer<'de>,
{
Ok(match String::deserialize(d)?.as_str() {
"No Outputs" => None,
"Today" => Some(Span::new()),
"Yesterday" => Some(Span::new().days(1)),
days if days.ends_with("days ago") => {
Some(Span::new().days(days.split(' ').next().unwrap().parse::<i64>().map_err(Error::custom)?))
}
weeks if weeks.ends_with("weeks ago") || weeks.ends_with("week ago") => {
Some(Span::new().weeks(weeks.split(' ').next().unwrap().parse::<i64>().map_err(Error::custom)?))
}
other => return Err(Error::custom(format!("Couldn't parse {other} as Span!"))),
})
}
fn bool_int<'de, D>(d: D) -> Result<bool, D::Error>
where
D: Deserializer<'de>,
{
Ok(match u8::deserialize(d)? {
0 => false,
1 => true,
_ => return Err(Error::custom("invalid value")),
})
}