Python example
This script runs a small house and a 5 MWp tracker field through pvperf's REST API.
The script needs Python 3.9 or later and the requests package. Install it with pip install requests. Set the environment variable PVPERF_API_KEY to your key from the workbench's Account page, then run python pvperf_first_run.py.
"""A first run of a small house and of a tracker field through pvperf's REST API.
Needs Python 3.9 or later and the requests package (pip install requests).
Set PVPERF_API_KEY to your key from the workbench's Account page, then run:
python pvperf_first_run.py
"""
import os
import requests
BASE_URL = os.environ.get("PVPERF_BASE_URL", "https://pvperf.com")
HEADERS = {"Authorization": "Bearer " + os.environ["PVPERF_API_KEY"]}
def post(path, body):
"""POST a JSON body to pvperf and return the answer. A refused request stops the script
with pvperf's reason. A refused run is not charged."""
r = requests.post(BASE_URL + path, json=body, headers=HEADERS, timeout=120)
answer = r.json()
if r.status_code >= 400:
raise SystemExit(f"{r.status_code} {answer['code']}: {answer['detail']}")
return answer
def show(name, answer):
s = answer["summary"]
print(f"{name}: {s['p_nom_dc_kwp']:,.1f} kWp, {s['e_grid_kwh']:,.0f} kWh to the grid a year")
print(f" specific yield {s['specific_yield_kwh_per_kwp']:,.0f} kWh/kWp")
print(f" performance ratio {s['performance_ratio']:.1%}")
losses = [r for r in answer["loss_diagram"] if r["kind"] == "loss" and r["unit"] == "kWh/kWp"]
for r in sorted(losses, key=lambda r: r["value"])[:3]:
print(f" {r['label']}: {r['value']:,.1f} kWh/kWp")
billing = answer["billing"]
print(f" charged {billing['units']} unit(s), USD {billing['price_usd']:.3f}")
# 1. A house: 16 modules of 400 W flush on a south-facing roof at 25° in Springfield, Illinois.
# The answer also carries the hourly power to the grid, p_grid_w, in W.
house = post(
"/v1/run/simple",
{
"latitude": 39.80,
"longitude": -89.65,
"racking": "rooftop",
"modules": 16,
"module_power_w": 400,
"tilt_deg": 25,
"azimuth_deg": 180,
"include_series": ["p_grid_w"],
},
)
show("House", house)
for month in house["summary"]["monthly"]:
print(f" month {month['month']:2d}: {month['e_grid_kwh']:7,.0f} kWh")
hourly_w = house["series"]["p_grid_w"]
print(f" {len(hourly_w)} hourly values, highest {max(hourly_w) / 1000:.2f} kW")
# 2. A tracker field: 5 MWp of single-axis trackers near Phoenix, Arizona.
field = post(
"/v1/run/simple",
{"latitude": 33.45, "longitude": -112.07, "racking": "single_axis", "system_capacity_kw": 5000},
)
show("Tracker field, ground cover ratio 0.40", field)
# 3. The same field with the rows further apart: change the full request the simple run
# returned and send it to POST /v1/run.
request = field["request"]
request["array"]["gcr"] = 0.33
wider = post("/v1/run", request)
show("Tracker field, ground cover ratio 0.33", wider)
What it prints
- For the house: the DC nameplate in kWp, the energy to the grid in kWh, the specific yield in kWh/kWp, the performance ratio, the three largest energy losses in kWh/kWp, the units charged and the energy to the grid of each month in kWh.
- The count of hourly values, 8,760, and the highest hourly power to the grid in kW.
- For the tracker field: the same summary at a ground cover ratio of 0.40, then at 0.33.
The script makes three runs. Each answer's billing block gives the units and the price of its run.
The requests
POST /v1/run/simpletakes a location and a system size. Its answer carriesrequest, the full request it ran, anddefaults_applied, each value it assumed with its source. The simple run guide lists every field.POST /v1/runtakes a full request. The script sends the simple run'srequestwith a newarray.gcr. The reference documents each field."include_series": ["p_grid_w"]adds the named hourly series to the answer.trueadds every series.
A refused request answers with a problem document. Its code and detail say why, and field names the field to fix. A run that fails is not charged.
A run predicted to take more than 40 s answers 202 with status running. GET /v1/runs/{run_id} returns it when it is done. Each run of this script answers in under 15 s.
The TypeScript example makes the same runs.