mod patterns;
use std::fmt::{Debug, Display};
use jiff::civil::Time;
use crate::{
Client,
parser::patterns::INVALID_TIME,
types::{
error::{
AddOutputError, AddStatusError, ClientError, DateTimeError, ExtendedDataField, NoEndpointSpecificError,
PostSystemError, Result,
},
params::{GetOutputParams, GetStatisticsParams, GetSystemParams},
response::{
AggregatedOutput, ConsumptionStatistics, DailyStatus, FavouriteSystem, FullSystem, Output, OutputExport,
Statistics, System, Tariffs,
},
},
};
pub fn daily_status(response: &str) -> Result<DailyStatus, NoEndpointSpecificError> {
let semicolons = response.chars().filter(|c| *c == ';').count();
let response = response.replace(';', "\n") + "\n";
let mut reader = Client::reader(&response);
let mut status: DailyStatus = reader.deserialize().next().ok_or(ClientError::MissingField)??;
if semicolons > 0 {
if response.lines().nth(1).is_some_and(|l| l.chars().filter(|c| *c == ',').count() == 3) {
status.consumption = Some(reader.deserialize().next().ok_or(ClientError::MissingField)??);
} else {
status.temperatures = Some(reader.deserialize().next().ok_or(ClientError::MissingField)??);
}
if semicolons > 1 {
status.temperatures = Some(reader.deserialize().next().ok_or(ClientError::MissingField)??);
}
}
Ok(status)
}
pub fn statistics(response: &str, params: &GetStatisticsParams) -> Result<Statistics, NoEndpointSpecificError> {
type Params = GetStatisticsParams;
let mut reader = Client::reader(response);
Ok(match params {
Params { system_id: Some(_), .. }
| Params {
consumption: false,
credit_and_debit: false,
..
} => {
reader.deserialize().next().ok_or(ClientError::MissingField)??
}
Params {
consumption: true,
credit_and_debit: true,
..
} => {
let (mut stats, consumption, credit, debit): (Statistics, ConsumptionStatistics, f32, f32) =
reader.deserialize().next().ok_or(ClientError::MissingField)??;
stats.consumption = Some(consumption);
stats.credit = Some(credit);
stats.debit = Some(debit);
stats
}
Params { consumption: true, .. } => {
let (mut stats, consumption): (Statistics, ConsumptionStatistics) =
reader.deserialize().next().ok_or(ClientError::MissingField)??;
stats.consumption = Some(consumption);
stats
}
Params {
credit_and_debit: true, ..
} => {
let (mut stats, credit, debit): (Statistics, f32, f32) =
reader.deserialize().next().ok_or(ClientError::MissingField)??;
stats.credit = Some(credit);
stats.debit = Some(debit);
stats
}
})
}
pub fn system(response: &str, params: &GetSystemParams) -> Result<FullSystem, NoEndpointSpecificError> {
let response = response.replace(';', "\n") + "\n";
let mut reader = Client::reader(&response);
let (subsystem, mut system) =
reader.deserialize::<(System, FullSystem)>().next().ok_or(ClientError::MissingField)??;
system.system = subsystem;
if params.tariffs
&& let Some(tariffs) = reader.deserialize::<Tariffs>().next()
{
system.tariffs = tariffs?;
}
if params.teams {
if let Some(teams) = reader.deserialize().next() {
system.teams = teams?;
}
}
system.donations = reader.deserialize().next().ok_or(ClientError::MissingField)??;
if params.extended_data
&& let Some(extended) = reader.deserialize().next()
{
system.extended_data = extended?;
}
if params.monthly_estimates
&& let Some(monthly_estimates) = reader.deserialize().next()
{
system.monthly_estimates = monthly_estimates?;
}
Ok(system)
}
pub fn outputs(response: &str, params: &GetOutputParams) -> Result<Vec<Output>, NoEndpointSpecificError> {
#[derive(serde::Deserialize)]
struct NanU32(#[serde(deserialize_with = "crate::types::response::opt_nan_u32")] Option<u32>);
let response = response.replace(';', "\n") + "\n";
let mut reader = Client::reader(&response);
let mut outputs = Vec::with_capacity(response.lines().count());
loop {
outputs.push(match (params.time_of_export, params.insolation) {
(true, true) => match reader.deserialize::<(Output, OutputExport, NanU32)>().next() {
Some(result) => {
let (mut output, export, insolation) = result?;
output.export = Some(export);
output.insolation = insolation.0;
output
}
None => break,
},
(false, true) => match reader.deserialize::<(Output, NanU32)>().next() {
Some(result) => {
let (mut output, insolation) = result?;
output.insolation = insolation.0;
output
}
None => break,
},
(true, false) => match reader.deserialize::<(Output, OutputExport)>().next() {
Some(result) => {
let (mut output, export) = result?;
output.export = Some(export);
output
}
None => break,
},
_ => match reader.deserialize::<Output>().next() {
Some(output) => output?,
None => break,
},
});
}
Ok(outputs)
}
pub fn aggregate_outputs(response: &str, export: bool) -> Result<Vec<AggregatedOutput>, NoEndpointSpecificError> {
let response = response.replace(';', "\n") + "\n";
let mut reader = Client::reader(&response);
let mut outputs = Vec::with_capacity(response.lines().count());
loop {
outputs.push(if export {
match reader.deserialize::<(AggregatedOutput, OutputExport)>().next() {
Some(result) => {
let (mut output, export) = result?;
output.export = Some(export);
output
}
None => break,
}
} else {
match reader.deserialize::<AggregatedOutput>().next() {
Some(output) => output?,
None => break,
}
});
}
Ok(outputs)
}
pub fn favourites(response: &str) -> Result<Vec<FavouriteSystem>, NoEndpointSpecificError> {
let mut reader = Client::reader(response);
Ok(
reader
.deserialize::<(u32, System, u32)>()
.map(|r| r.map(|(id, system, interval)| FavouriteSystem::new(system, id, interval)))
.collect::<core::result::Result<_, _>>()?,
)
}
pub async fn error<F, Endpoint>(response: reqwest::Response, endpoint_parser: F) -> ClientError<Endpoint>
where
F: FnOnce(&str) -> Option<Endpoint>,
Endpoint: Debug + Display,
{
let status = response.status();
let response = response.text().await;
response.map_or_else(
|_| ClientError::UnrecognisedStatusCode(status, None),
|response| {
match response.trim().trim_start_matches("Bad request 400: ").trim_start_matches("Bad Request 400: ") {
"Unauthorized 401: Invalid System ID" => ClientError::InvalidSystemId,
"Unauthorized 401: Invalid API Key" => ClientError::InvalidApiKey,
"Unauthorized 401: Disabled API Key" => ClientError::DisabledApiKey,
"Forbidden 403: Read only key" => ClientError::ReadOnlyKey,
"Unauthorized 401: Missing, invalid or inactive api key information (X-Pvoutput-Apikey)" => {
ClientError::BadApiKey
}
"Forbidden 403: Exceeded number requests per hour" => ClientError::RateLimitExceeded,
"Forbidden 403: Donation mode" => ClientError::DonationModeRequired,
"Unauthorized 401: Inaccessible System ID" => ClientError::InaccessibleSystemId,
"Status not found" => ClientError::NotFound,
e if e.starts_with("No") && e.ends_with("found") => ClientError::NotFound,
e => endpoint_parser(e).map_or_else(
|| ClientError::UnrecognisedStatusCode(status, Some(response)),
|e| ClientError::EndpointSpecific(e),
),
}
},
)
}
pub const fn parse_none(_error: &str) -> Option<NoEndpointSpecificError> { None }
pub fn parse_datetime_error(error: &str) -> Option<DateTimeError> {
Some(if error == "Date missing" || error == "Date is required" {
DateTimeError::MissingDateOrTime
} else if let Some(Ok(date)) = patterns::DATE_TOO_OLD.captures(error).map(|c| c[1].parse()) {
DateTimeError::DateTooOld(date)
} else if let Some(Ok(date)) = patterns::DATE_TOO_NEW.captures(error).map(|c| c[1].parse()) {
DateTimeError::DateTooNew(date)
} else if let Some(date) = patterns::INVALID_DATE
.captures(error)
.map(|c| c.get(1).map_or_else(|| c[2].to_string(), |g| g.as_str().to_string()))
{
DateTimeError::InvalidDate(date)
} else if let Some(time) = INVALID_TIME
.captures(error)
.map(|c| c.get(1).map_or_else(|| c[2].to_string(), |g| g.as_str().to_string()))
{
DateTimeError::InvalidTime(time)
} else {
return None;
})
}
pub fn parse_add_output_error(error: &str) -> Option<AddOutputError> {
Some(
if error == "No outputs added"
|| error == "Missing parameter [data]"
|| error == "No statuses added or updated"
|| error == "Invalid data format []"
{
AddOutputError::NoData
} else if let Some(datetime_error) = parse_datetime_error(error) {
AddOutputError::InvalidDateTime(datetime_error)
} else if let Some((which, Ok(value), Ok(system_size), Ok(date))) = patterns::GENERATION_OR_EXPORT_TOO_HIGH
.captures(error)
.map(|c| (c[1].to_string(), c[2].parse(), c[3].parse(), c[4].parse()))
{
if which == "Generation" {
AddOutputError::GenerationTooHigh {
generation: value,
system_size,
date,
}
} else {
debug_assert_eq!(which, "Export");
AddOutputError::ExportTooHigh {
export: value,
system_size,
date,
}
}
} else if let Some((Ok(export), Ok(generation))) =
patterns::EXPORT_EXCEEDS_GENERATION.captures(error).map(|c| (c[1].parse(), c[2].parse()))
{
AddOutputError::ExportExceedsGeneration { export, generation }
} else if let Some((Ok(consumption), Ok(date))) =
patterns::CONSUMPTION_TOO_HIGH.captures(error).map(|c| (c[1].parse(), c[2].parse()))
{
AddOutputError::ConsumptionTooHigh(consumption, date)
} else if let Some((Ok(peak_power), Ok(system_size), Ok(date))) =
patterns::PEAK_POWER_TOO_HIGH.captures(error).map(|c| (c[1].parse(), c[2].parse(), c[3].parse()))
{
AddOutputError::PeakPowerTooHigh {
peak_power,
system_size,
date,
}
} else if let Some((which, Ok(date))) =
patterns::TEMPERATURE_MISSING.captures(error).map(|c| (c[1].to_string(), c[2].parse()))
{
if which == "Max" {
AddOutputError::MinTemperatureWithoutMax(date)
} else {
debug_assert_eq!(which, "Min");
AddOutputError::MaxTemperatureWithoutMin(date)
}
} else {
return None;
},
)
}
pub fn parse_add_status_error(error: &str) -> Option<AddStatusError> {
Some(match error {
"No outputs added" | "Missing parameter [data]" | "No statuses added or updated" | "Invalid data format []" => {
AddStatusError::NoData
}
"Missing energy and power values" => AddStatusError::MissingFields,
e if e.starts_with("Missing required data") || e.starts_with("Missing parameter") => AddStatusError::MissingFields,
"Moon Powered" => AddStatusError::MoonPowered,
"Invalid net and cumulative" => AddStatusError::BothCumulativeAndNet,
"Missing net power value" => AddStatusError::NoEnergyData,
e => {
if let Some(datetime_error) = parse_datetime_error(e) {
AddStatusError::InvalidDateTime(datetime_error)
} else if let Some((which, Ok(value), Ok(system_size))) =
patterns::PRODUCTION_TOO_HIGH.captures(e).map(|c| (c[1].to_string(), c[2].parse(), c[3].parse()))
{
if which == "Power" {
AddStatusError::ExcessivePowerGeneration {
power: value,
system_size,
}
} else {
debug_assert_eq!(which, "Energy");
AddStatusError::ExcessiveEnergyGeneration {
energy: value,
system_size,
}
}
} else if let Some((Ok(value), Ok(hour), Ok(minute))) =
patterns::ENERGY_TOO_HIGH_FOR_TIME.captures(e).map(|c| (c[1].parse(), c[2].parse(), c[3].parse()))
{
AddStatusError::EnergyTooHighForTime(value, Time::new(hour, minute, 0, 0).expect("time should be valid"))
} else if let Some(data) = patterns::INVALID_DATA_FORMAT.captures(e).map(|c| c[1].to_string()) {
AddStatusError::InvalidDataFormat(data)
} else {
return None;
}
}
})
}
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
pub fn parse_post_system_error(error: &str) -> Option<PostSystemError> {
Some(if error == "System name is too long" {
PostSystemError::SystemNameTooLong
} else if let Some((Ok(v), which)) =
patterns::MISSING_EXTENDED_DATA_FIELD.captures(error).map(|c| (c[1].parse(), c[2].to_string()))
{
if which == "unit" {
PostSystemError::MissingExtendedDataUnit(v)
} else {
debug_assert_eq!(which, "label");
PostSystemError::MissingExtendedDataLabel(v)
}
} else if let Some((Ok(v), which)) =
patterns::EXTENDED_DATA_FIELD_TOO_LONG.captures(error).map(|c| (c[1].parse(), c[2].to_string()))
{
if which == "unit" {
PostSystemError::ExtendedDataUnitTooLong(v)
} else {
debug_assert_eq!(which, "label");
PostSystemError::ExtendedDataLabelTooLong(v)
}
} else if let Some(Ok(v)) = patterns::INVALID_EXTENDED_DATA_GRAPH.captures(error).map(|c| c[1].parse()) {
PostSystemError::InvalidExtendedDataField(v, ExtendedDataField::Graph)
} else if let Some(Ok(v)) = patterns::INVALID_EXTENDED_DATA_AXIS.captures(error).map(|c| c[1].parse()) {
PostSystemError::InvalidExtendedDataField(v, ExtendedDataField::Axis)
} else if let Some(Ok(v)) = patterns::INVALID_EXTENDED_DATA_COLOUR.captures(error).map(|c| c[1].parse()) {
PostSystemError::InvalidExtendedDataField(v, ExtendedDataField::Colour)
} else {
return None;
})
}