pvoutput-client 0.0.3

Client for the PVOutput.org API
Documentation
//! Read-only API endpoint methods.

use const_format::formatcp;
use jiff::civil::Date;
use serde::Serialize;

use crate::{
	API_BASE_URL, Client, parser,
	parser::parse_none,
	types::{
		error::NoEndpointSpecificError,
		params::{
			Aggregation, GetExtendedParams, GetInsolationParams, GetOutputParams, GetOutputParamsOuter, GetStatisticsParams,
			GetStatusParams, GetStatusParamsOuter, GetSystemParams,
		},
		regions::Region,
		response::{
			AggregatedOutput, DailyStatus, ExtendedData, FavouriteSystem, FullSystem, History, Insolation, Ladder, Output,
			Response, Statistics, Status, Supply, Team, TeamOutput,
		},
	},
};

impl Client {
	/// Retrieves a single status for the specified system.
	///
	/// - For historic data (`h`), use [`Client::get_status_history`]
	/// - For statistics (`stats`), use [`Client::get_status_daily`]
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#get-status-service)
	pub async fn get_status(&self, params: GetStatusParams) -> crate::Result<Response<Status>, NoEndpointSpecificError> {
		self
			.get_single(
				formatcp!("{API_BASE_URL}/getstatus.jsp"),
				GetStatusParamsOuter::new(params, false, false),
				parse_none,
			)
			.await
	}

	/// Retrieves historic statuses for the specified system.
	///
	/// - For a single measurement, use [`Client::get_status`]
	/// - For statistics (`stats`), use [`Client::get_status_daily`]
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#get-status-service)
	pub async fn get_status_history(
		&self,
		params: GetStatusParams,
	) -> crate::Result<Response<Vec<History>>, NoEndpointSpecificError> {
		self
			.get_multiple(
				formatcp!("{API_BASE_URL}/getstatus.jsp"),
				GetStatusParamsOuter::new(params, true, false),
				parse_none,
			)
			.await
	}

	/// Retrieves a daily stats summary for the specified system.
	///
	/// - For historic data (`h`), use [`Client::get_status_history`]
	/// - For a single measurement, use [`Client::get_status`]
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#get-status-service)
	pub async fn get_status_daily(
		&self,
		params: GetStatusParams,
	) -> crate::Result<Response<DailyStatus>, NoEndpointSpecificError> {
		let (rate_limit, response) = self
			.get(
				formatcp!("{API_BASE_URL}/getstatus.jsp"),
				GetStatusParamsOuter::new(params, false, true),
				parse_none,
			)
			.await?;

		Ok(Response::new(parser::daily_status(&response)?, rate_limit))
	}

	/// Retrieves statistics for a system.
	///
	/// This endpoint has a rate limit of **twelve** requests per hour for free accounts, and **sixty** for accounts with
	/// [donation status](https://pvoutput.org/help/donations.html).
	///
	/// This information is not exposed by the API -- the `rate_limit` on the `Response` struct will not reflect these
	/// limits.
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#get-statistic-service)
	pub async fn get_statistics(
		&self,
		params: GetStatisticsParams,
	) -> crate::Result<Response<Statistics>, NoEndpointSpecificError> {
		let (rate_limit, response) = self.get(formatcp!("{API_BASE_URL}/getstatistic.jsp"), &params, parse_none).await?;

		Ok(Response::new(parser::statistics(&response, &params)?, rate_limit))
	}

	/// Retrieves system information.
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#get-system-service)
	pub async fn get_system(
		&self,
		params: GetSystemParams,
	) -> crate::Result<Response<FullSystem>, NoEndpointSpecificError> {
		let (rate_limit, response) = self.get(formatcp!("{API_BASE_URL}/getsystem.jsp"), &params, parse_none).await?;

		Ok(Response::new(parser::system(&response, &params)?, rate_limit))
	}

	/// Retrieves a system's ladder ranking.
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#get-ladder-service)
	///
	/// # Parameters
	/// - `system`: System ID to retrieve status for.
	///   If unset, the [`Client`]'s authenticated system is used.
	///   Requires [donation status](https://pvoutput.org/help/donations.html).
	pub async fn get_ladder(&self, system: Option<u32>) -> crate::Result<Response<Ladder>, NoEndpointSpecificError> {
		self.get_single(formatcp!("{API_BASE_URL}/getladder.jsp"), &[("sid1", system)], parse_none).await
	}

	/// Retrieves daily outputs.
	///
	/// - For aggregated outputs (`a`), use [`Client::get_aggregate_outputs`]
	/// - For team outputs (`tid`), use [`Client::get_team_outputs`]
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#get-output-service)
	pub async fn get_outputs(
		&self,
		params: GetOutputParams,
	) -> crate::Result<Response<Vec<Output>>, NoEndpointSpecificError> {
		let params = GetOutputParamsOuter {
			params,
			..Default::default()
		};

		let (rate_limit, response) = self.get(formatcp!("{API_BASE_URL}/getoutput.jsp"), &params, parse_none).await?;
		Ok(Response::new(parser::outputs(&response, &params.params)?, rate_limit))
	}

	/// Retrieves monthly or yearly aggregated outputs.
	///
	/// - For daily outputs, use [`Client::get_outputs`]
	/// - For team outputs (`tid`), use [`Client::get_team_outputs`]
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#get-output-service)
	pub async fn get_aggregate_outputs(
		&self,
		aggregation: Aggregation,
		params: GetOutputParams,
	) -> crate::Result<Response<Vec<AggregatedOutput>>, NoEndpointSpecificError> {
		let params = GetOutputParamsOuter {
			params,
			a: Some(aggregation),
			..Default::default()
		};

		let (rate_limit, response) = self.get(formatcp!("{API_BASE_URL}/getoutput.jsp"), &params, parse_none).await?;
		Ok(Response::new(parser::aggregate_outputs(&response, params.params.time_of_export)?, rate_limit))
	}

	/// Retrieves daily outputs for the given team.
	///
	/// - For daily outputs, use [`Client::get_outputs`]
	/// - For aggregated outputs (`a`), use [`Client::get_aggregate_outputs`]
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#get-output-service)
	pub async fn get_team_outputs(
		&self,
		team_id: u32,
		params: GetOutputParams,
	) -> crate::Result<Response<Vec<TeamOutput>>, NoEndpointSpecificError> {
		self
			.get_multiple(
				formatcp!("{API_BASE_URL}/getoutput.jsp"),
				GetOutputParamsOuter {
					params,
					tid: Some(team_id),
					..Default::default()
				},
				parse_none,
			)
			.await
	}

	/// Retrieves daily extended output values for your system.
	///
	/// Requires [donation status](https://pvoutput.org/help/donations.html).
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#get-extended-service)
	///
	/// # Errors
	/// Returns [`ClientError::NotFound`](crate::ClientError::NotFound) if your system has no extended data for the
	/// specified date range.
	pub async fn get_extended(
		&self,
		params: GetExtendedParams,
	) -> crate::Result<Response<Vec<ExtendedData>>, NoEndpointSpecificError> {
		self.get_multiple(formatcp!("{API_BASE_URL}/getextended.jsp"), &params, parse_none).await
	}

	/// Gets a system's favourite systems.
	/// Limited to fifty systems.
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#get-favourite-service)
	///
	/// # Parameters
	/// - `system`: System ID to retrieve favourites for.
	///   If unset, the [`Client`]'s authenticated system is used.
	///   Requires [donation status](https://pvoutput.org/help/donations.html).
	pub async fn get_favourites(
		&self,
		system: Option<u32>,
	) -> crate::Result<Response<Vec<FavouriteSystem>>, NoEndpointSpecificError> {
		let (rate_limit, response) =
			self.get(formatcp!("{API_BASE_URL}/getfavourite.jsp"), &[("sid1", system)], parse_none).await?;
		Ok(Response::new(parser::favourites(&response)?, rate_limit))
	}

	/// Retrieves a list of missing output dates for your system.
	///
	/// A maximum of **fifty** missing dates will be returned in ascending order (i.e., oldest to most recent).
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#get-missing-service)
	pub async fn get_missing(
		&self,
		from: Option<Date>,
		to: Option<Date>,
	) -> crate::Result<Response<Vec<Date>>, NoEndpointSpecificError> {
		self
			.get_single(
				formatcp!("{API_BASE_URL}/getmissing.jsp"),
				&[
					("df", from.map(|d| d.strftime("%Y%m%d").to_string())),
					("dt", to.map(|d| d.strftime("%Y%m%d").to_string())),
				],
				parse_none,
			)
			.await
	}

	/// Retrieves insolation data at five minute intervals.
	///
	/// Requires [donation status](https://pvoutput.org/help/donations.html).
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#get-insolation-service)
	pub async fn get_insolation(
		&self,
		params: GetInsolationParams,
	) -> crate::Result<Response<Vec<Insolation>>, NoEndpointSpecificError> {
		self.get_multiple(formatcp!("{API_BASE_URL}/getinsolation.jsp"), &params, parse_none).await
	}

	/// Retrieves team data.
	///
	/// Only team owners may retrieve details of their own team.
	/// [Donation-enabled](https://pvoutput.org/help/donations.html) accounts may query any team.
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#get-team-service)
	pub async fn get_team(&self, team: u32) -> crate::Result<Response<Team>, NoEndpointSpecificError> {
		// why is this a semicolon-delimited response??
		let (rate_limit, response) =
			self.get(formatcp!("{API_BASE_URL}/getteam.jsp"), &[("tid", team)], parse_none).await?;

		Ok(Response::new(
			Self::semicolon_reader(&response).deserialize().collect::<csv::Result<Vec<_>>>()?.remove(0),
			rate_limit,
		))
	}

	/// Retrieves aggregated live generation and consumption data.
	///
	/// # Parameters
	/// - `region`: Region to retrieve data for.
	///   If provided, the 24-hour supply and demand history for the region will be returned.
	///   Requires [donation status](https://pvoutput.org/help/donations.html).
	/// - `timezone`: Timezone to use in responses.
	///   If unset, data will be returned in UTC.
	///
	/// [PVOutput docs](https://pvoutput.org/help/api_specification.html#get-supply-service)
	pub async fn get_supply(
		&self,
		region: Option<Region>,
		timezone: Option<String>,
	) -> crate::Result<Response<Vec<Supply>>, NoEndpointSpecificError> {
		#[derive(Serialize)]
		struct Params {
			r: Option<Region>,
			tz: Option<String>,
		}

		let params = Params {
			r: region,
			tz: timezone,
		};
		self.get_multiple(formatcp!("{API_BASE_URL}/getsupply.jsp"), &params, parse_none).await
	}
}