Build a wind fleet end to end
Create a fleet of sites, load a year of hourly production in one call, read it all back chart-ready, and check its coverage.
This tutorial exercises the whole API in one realistic flow: authenticate, create a fleet of sites with their assets, upload a year of hourly production for all of them in one call, read every site back in one call and sum a fleet total, then check the data's coverage and current values.
It is modelled on the open NVE dataset of Norwegian wind farms, which has 62 sites and a year of hourly production for each. To keep the code self-contained, this page uses three sites and generates a stand-in production series in the same shape. Swap in your own data where the comments say so. It assumes the BASE and HEADERS setup from Authentication.
1. Confirm access
r = requests.get(f"{BASE}/objects", headers=HEADERS)
r.raise_for_status()
assert "sites" in [t["name"] for t in r.json()["items"]]2. Create the fleet
One POST /objects/sites per park, each with its turbines inline. A site and its assets are created atomically, so a failure leaves nothing half-made.
FLEET = [
# name, latitude, longitude, turbine model, count, kW per turbine
("Smola", 63.40, 8.00, "Siemens 2.3", 68, 2300),
("Hitra", 63.55, 8.78, "Siemens 2.3", 24, 2300),
("Utsira", 59.31, 4.89, "Enercon E-40", 2, 600),
]
sites = []
for name, lat, lon, model, count, kw in FLEET:
r = requests.post(f"{BASE}/objects/sites", headers=HEADERS, json={
"name": name,
"values": {
"latitude": lat,
"longitude": lon,
"assets": [
{"name": f"{model} x{count}", "values": {"type": "wind_turbine", "capacity_kw": count * kw}},
],
},
})
r.raise_for_status()
sites.append(r.json())
fleet_kw = sum(s["stats"]["total_capacity_kw"] for s in sites)
print(f"{len(sites)} sites, {fleet_kw / 1000:.1f} MW")3 sites, 212.8 MWEach site record carries the generated id that every later call keys on.
3. Upload a year of hourly production in one call
Production is a singleindex series in MW. The helper below generates a plausible hourly series for the year from a site's capacity; replace production_for with a function that reads your own data.
Rather than one upload per site, POST /timeseries/write takes every site's series in one batch and lands them whole or not at all. Three sites times 8784 hours is 26,352 values, well inside the 1,000,000-value budget.
import math
import random
from datetime import datetime, timedelta, timezone
HOURS = 24 * 366 # 2024 is a leap year
T0 = datetime(2024, 1, 1, tzinfo=timezone.utc)
def production_for(capacity_mw: float) -> list[dict]:
"""Stand-in for real data: seasonal + daily cycle with noise, in MW."""
random.seed(capacity_mw)
points = []
for h in range(HOURS):
season = 0.35 + 0.15 * math.cos(2 * math.pi * h / HOURS) # windier in winter
daily = 0.05 * math.sin(2 * math.pi * (h % 24) / 24)
cf = max(0.0, min(1.0, season + daily + random.gauss(0, 0.12)))
points.append({
"valid_time": (T0 + timedelta(hours=h)).isoformat(),
"value": round(capacity_mw * cf, 3),
})
return points
r = requests.post(f"{BASE}/timeseries/write", headers=HEADERS, json={
"series": [
{
"record": site["id"],
"type": "power-production",
"shape": "singleindex",
"unit": "MW",
"points": production_for(site["stats"]["total_capacity_kw"] / 1000),
}
for site in sites
],
})
r.raise_for_status()
result = r.json()
print(result["points_written"], "written,", result["points_skipped"], "skipped")26352 written, 0 skippedRun the cell again and it reports 0 written, 26352 skipped: the store already holds those values, so a retry after a network error needs no cleanup first.
4. Read the whole fleet back, chart-ready
A year of hourly data is 8784 points per site. One POST /timeseries/read fetches all three with six-hour bucket means, decided once for the whole batch, in columnar form.
r = requests.post(f"{BASE}/timeseries/read", headers=HEADERS, json={
"series": [{"record": site["id"], "type": "power-production"} for site in sites],
"start": "2024-01-01T00:00:00Z",
"end": "2024-12-31T23:00:00Z",
"bucket_seconds": 6 * 3600,
"format": "columns",
})
r.raise_for_status()
items = r.json()["items"]
print(len(items), "series,", len(items[0]["times"]), "buckets each")3 series, 1464 buckets eachEvery item shares the same bucket grid, so the fleet total is a column-wise sum:
times = items[0]["times"]
fleet = [sum(item["values"]["value"][i] for item in items) for i in range(len(times))]Plotting is a few lines away:
import matplotlib.pyplot as plt
plt.plot(times, fleet)
plt.ylabel("Fleet production (MW)")
plt.show()For a raw export, drop bucket_seconds and format and page through the year with explicit start/end windows instead.
5. Check coverage and current values
Two more batch routes answer the questions a monitoring job asks. POST /timeseries/stats with daily buckets says how many days of the year hold data for each site:
r = requests.post(f"{BASE}/timeseries/stats", headers=HEADERS, json={
"series": [{"record": site["id"], "type": "power-production"} for site in sites],
"start": "2024-01-01T00:00:00Z",
"end": "2024-12-31T23:00:00Z",
"bucket_seconds": 86400,
})
r.raise_for_status()
for item, site in zip(r.json()["items"], sites):
stats = item["columns"]["value"]
print(f"{site['name']:<8} {len(stats['buckets'])} of 366 days, last point {stats['extent']['last']}")POST /timeseries/latest gives the most recent value per site without reading a window:
r = requests.post(f"{BASE}/timeseries/latest", headers=HEADERS, json={
"series": [{"record": site["id"], "type": "power-production"} for site in sites],
})
r.raise_for_status()
for item, site in zip(r.json()["items"], sites):
point = item["latest"]["value"]
print(f"{site['name']:<8} {point['value']} MW at {point['valid_time']}")6. Clean up (optional)
Deleting a site deletes its assets and every series you uploaded to it:
for site in sites:
requests.delete(f"{BASE}/objects/sites/{site['id']}", headers=HEADERS).raise_for_status()What you used
- Sites: atomic create with inline assets,
stats.total_capacity_kw. - Many series at once: batch write with dedup, batch read with shared bucketing and
format=columns, stats, latest. - Upload / read timeseries: the
singleindexshape and what a re-upload does.