pvoutput-client 0.0.3

Client for the PVOutput.org API
Documentation
use jiff::{Zoned, tz::TimeZone};
use pvoutput_client::{
	ClientError,
	client::Client,
	types::params::{
		Aggregation, GetExtendedParams, GetInsolationParams, GetOutputParams, GetStatisticsParams, GetStatusParams,
		GetSystemParams,
	},
};
use rand::random_bool;

#[tokio::main]
#[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
	dotenvy::dotenv()?;
	let (Ok(api_key), Ok(system)) = (std::env::var("PVO_API_KEY"), std::env::var("PVO_SYSTEM")) else {
		panic!("PVO_API_KEY and PVO_SYSTEM must be set! Use .env file or export them.")
	};

	let Ok(system) = system.parse() else {
		panic!("PVO_SYSTEM must be a valid system ID!")
	};

	let client = Client::new(&api_key, system, None, None)?;

	let params = GetSystemParams {
		donations: random_bool(0.5),
		teams: random_bool(0.5),
		monthly_estimates: random_bool(0.5),
		extended_data: random_bool(0.5),
		secondary_array: random_bool(0.5),
		tariffs: random_bool(0.5),
		..Default::default()
	};

	// println!("System params: {params:?}");
	let full_system = client.get_system(params).await?.data;
	let system = &full_system.system;
	println!("Retrieving data for {} ({} system)...", system.name, format_watts(system.size as f32));

	let status = client.get_status(GetStatusParams::default()).await?;

	let status = status.data;
	println!(
		"On {} at {}, {} generated {} of power.",
		status.date,
		status.time,
		system.name,
		format_watts(status.power_generation)
	);

	let daily_status = client.get_status_daily(GetStatusParams::default()).await?.data;
	println!(
		"On this system's most recent productive day, power generation peaked at {} with a value of {}!",
		daily_status.peak_time,
		format_watts(daily_status.peak_power_generation)
	);

	let history = client.get_status_history(GetStatusParams::default()).await?.data;
	println!(
		"Found {} history entries; latest at {}, with energy efficiency of {:.2} kWh/kWh",
		history.len(),
		history[0].time,
		history[0].energy_efficiency
	);

	let stats = client.get_statistics(GetStatisticsParams::default()).await?.data;
	println!(
		"{} generated {}h of energy between {} and {}!",
		system.name,
		format_watts(stats.energy_generated as f32),
		stats.date_from,
		stats.date_to
	);

	let outputs = client.get_outputs(GetOutputParams::default()).await?.data;
	println!("Weather for {} was {:?}", outputs[0].date, outputs[0].conditions);

	let aggregate = client.get_aggregate_outputs(Aggregation::Yearly, GetOutputParams::default()).await?.data;
	println!(
		"{} generated {} in {}",
		system.name,
		format_watts(aggregate[0].energy_generation as f32),
		aggregate[0].date.year()
	);

	print!("Latest extended data: ");
	let extended = client.get_extended(GetExtendedParams::default()).await;
	if matches!(extended, Err(ClientError::NotFound)) {
		println!("none!");
	} else {
		println!("{:?}", extended?.data);
	}

	let missing = client.get_missing(None, None).await?.data;
	if missing.is_empty() {
		println!("No missing outputs :)");
	} else {
		println!("Found {} days with missing outputs, dating back to {}.", missing.len(), missing[0]);
	}

	let insolation = client.get_insolation(GetInsolationParams::default()).await;
	if let Ok(insolation) = insolation {
		println!("Retrieved {} insolation results.", insolation.data.len());
	} else if !matches!(insolation, Err(ClientError::DonationModeRequired)) {
		insolation?;
	}

	println!();
	let favourites = client.get_favourites(None).await?.data;
	println!(
		"{} has {} favourite systems{}",
		system.name,
		favourites.len(),
		if favourites.is_empty() { "." } else { ":" }
	);
	for favourite in favourites {
		println!(
			" - {} (ID {}): {} system",
			favourite.system.name,
			favourite.id,
			format_watts(favourite.system.size as f32)
		);
	}

	println!();
	let team_id = full_system.teams.first().copied().unwrap_or(1);
	let team = client.get_team(team_id).await?.data;
	println!(
		"Team {} has generated {} across {} outputs from {} systems!",
		team.name,
		format_watts(team.energy_generation as f32),
		team.outputs,
		team.systems
	);

	let ladder = client.get_ladder(None).await?;

	println!();
	println!("Ladder position as of {}:", ladder.data.date);
	if let Some(rank) = ladder.data.generation_rank {
		println!(" - Generation rank: {rank}. View online at {}", ladder.data.generation_rank_url().unwrap());
	}
	println!(" - Efficiency rank: {}", ladder.data.efficiency_rank.unwrap_or_default());
	println!(" - Last output considered: {}", ladder.data.last_output.unwrap_or_default());
	println!();

	let reset = Zoned::new(ladder.rate_limit.reset, TimeZone::system());
	println!(
		"Rate limit: {} per hour, {} remaining, resetting at {} (in {} minutes)",
		ladder.rate_limit.limit,
		ladder.rate_limit.remaining,
		reset,
		Zoned::now().until(&reset).unwrap().get_minutes()
	);

	Ok(())
}

fn format_watts(watts: f32) -> String {
	match watts {
		w if w.is_nan() => String::from("zero W"),
		..1000.0 => format!("{watts:.2} W"),
		..1_000_000.0 => format!("{:.2} kW", watts / 1000.0),
		..1_000_000_000.0 => format!("{:.2} MW", watts / 1_000_000.0),
		_ => format!("{:.2} GW", watts / 1_000_000_000.0),
	}
}