Generatore di report esterni
Questo esempio preleva da Mirox i dati di un impianto e ne ricava un report proprio: prima le tre richieste con curl, poi un generatore di report in Python completo che le trasforma in un file PDF.
Cerchi invece il lavoro con i fogli di calcolo?
Per Excel — curve di produzione, più impianti affiancati, singoli inverter, sensori, turbine — vedi Metric Export per Excel.
Prerequisiti
- Un account Mirox che possa leggere l'impianto.
- L'UID dell'impianto — l'identificatore univoco della risorsa impianto su cui vuoi creare il report.
- Un token API del gruppo Metric Export — vedi Utilizzo dei token API.
Le tre richieste
| Dati | Richiesta | Risposta |
|---|---|---|
| Informazioni sull'impianto | GET /v1/export/report/{park_uid}/info | JSON |
| Eventi | GET /v1/export/report/{park_uid}/events.csv | CSV |
| Valori di produzione | GET /v1/export/series/template/{template_uid} | CSV, oppure JSON con format=json |
Tutti i percorsi seguono https://service.mirox.io/api. I parametri della terza richiesta sono descritti nella Metric Export API, le prime due nell'API di esportazione legacy.
Esempio 1: informazioni sull'impianto
curl "https://service.mirox.io/api/v1/export/report/ABC123DEF456/info" \
-H "Authorization: Bearer YOUR_API_TOKEN"
{
"uid": "ABC123DEF456",
"name": "Sunnyside Solar Park",
"type": "solar",
"description": "Sunnyside Solar Park is a 42 MW photovoltaic installation...",
"latitude": 48.7511,
"longitude": 9.1225,
"timezone": "Europe/Berlin",
"peak_production_w": 42000000,
"active": true,
"created_at": "2024-05-16T10:30:25.104830Z",
"portfolio": {
"uid": "GHI789JKL012",
"name": "Renewable South"
},
"organization": {
"uid": "MNO345PQR678",
"name": "GreenPower Inc."
},
"address": {
"address1": "123 Solar Way",
"zip_code": "70123",
"city": "Sunnyville",
"country": "Germany"
}
}
Esempio 2: eventi
curl "https://service.mirox.io/api/v1/export/report/ABC123DEF456/events.csv?year=2026&quarter=1&separator_csv=,&separator_decimal=." \
-H "Authorization: Bearer YOUR_API_TOKEN"
Event ID,Start Date,End Date,Duration (hours),Type,Creator,Description,Priority
9876543210123456789,2026-01-15 08:45,Ongoing,720.25,Sensor Error,System,Irradiation sensor malfunction detected,1200
8765432101234567890,2026-02-10 14:30,2026-02-15 09:15,114.75,Data Availability,System,Connection issues with data acquisition system,1100
year,quarteremonthscelgono il periodo. Senza di essi, l'esportazione copre l'anno corrente.- Senza i due parametri dei separatori, le colonne sono separate da
;e il segno decimale è,. - La data di fine di un evento ancora aperto è
Ongoing.
Filtra gli eventi per priorità
Una Priority di 1000 o più contrassegna un evento importante, come uno spegnimento che costa produzione o un allarme. I valori più bassi sono informativi, come una connessione che è tornata.
Esempio 3: valori mensili di produzione
Il modello di sistema Report tecnico (ABCD12340002) per il primo trimestre, per mese, con gli ID come intestazioni di colonna:
curl --compressed "https://service.mirox.io/api/v1/export/series/template/ABCD12340002?park=ABC123DEF456&year=2026&quarter=1&step=month&header=id&separator_csv=,&separator_decimal=." \
-H "Authorization: Bearer YOUR_API_TOKEN"
timestamp,plant.grid_feedin,plant.rad_pyranometer,plant.rad_weather,availability.sunhours,availability.inverter_park,plant.shutdown_grid,plant.shutdown_external,plant.expected_weather,plant.expected_pyranometer,plant.pr_weather,plant.pr_pyranometer,plant.specific_yield
2026-01,950000.4,18.4,18.1,62.5,99.1,15000.5,25000.8,980000.0,975000.0,81.2,82.0,22.6
2026-02,1250000.3,22.8,22.5,96.0,99.4,2000.3,18500.4,1300000.0,1290000.0,81.0,81.6,29.8
2026-03,1850000.8,35.4,35.0,148.3,98.9,45000.2,30000.7,1900000.0,1885000.0,80.7,81.3,44.0
header=iddà intestazioni di colonna che non cambiano mai. Conheader=name, il valore predefinito, le intestazioni sono i nomi delle metriche nella lingua che richiedi, comeGrid feed-in (kWh).- Una cella vuota significa che non c'è alcun valore. Non significa zero.
- Più impianti o un intero portfolio entrano in una sola richiesta:
park=ABC123DEF456,DEF456ABC123oppureportfolio=<uid>, conmulti_park_agg=splitper una colonna per impianto. - Le colonne del modello sono elencate nella guida alla migrazione.
Molti impianti su un periodo lungo
Un impianto su un anno intero entra in una sola richiesta, e così 25 impianti. Una richiesta troppo grande risponde 400 over_budget e nomina il periodo più lungo che ci sta. In tal caso richiedi un trimestre alla volta, oppure un portfolio alla volta, e unisci i file. Vedi Dividere un'esportazione grande.
Il modello precedente
I report costruiti sul modello precedente Report Technical v1 continuano a funzionare. Il suo link e le sue colonne sono invariati:
curl "https://service.mirox.io/api/v1/export/metrics/template/ABCD12340001?park=ABC123DEF456&resolution=monthly" \
-H "Authorization: Bearer YOUR_API_TOKEN"
Che cosa differisce nel successore ABCD12340002:
ABCD12340001 (legacy) | ABCD12340002 | |
|---|---|---|
| Link | /v1/export/metrics/template/… | /v1/export/series/template/… |
| Intestazioni di colonna | i nomi dell'esportazione precedente, come Energy Production (kWh) | i nomi della raccolta di metriche, come Grid feed-in (kWh), oppure gli ID con header=id |
| Prima colonna | Date (YYYY-MM) | Date, oppure timestamp con header=id |
| Colonna aggiuntiva | Days in Month (d) con risoluzione mensile | nessuna |
| Valore mancante | 0 | vuoto |
| Giorni e mesi | UTC | l'ora locale dell'impianto |
| Colonne calcolate | incluse | il performance ratio, la produzione attesa e la resa specifica sono inclusi; le altre restano fuori |
Usare le richieste in altri strumenti
- Python: la libreria
requests, come nello script più sotto. - Excel: Power Query con l'header
Authorization— vedi Excel con Power Query. - Power BI: la stessa connessione Power Query di Excel.
- Tableau e altri: qualsiasi strumento che legga un link CSV o JSON e possa inviare un header.
Risoluzione dei problemi
| Risposta | Significato | Che cosa fare |
|---|---|---|
401 | Il token manca, è scaduto o è stato revocato. | Crea un nuovo token, oppure ruota quello esistente. |
403 no_accessible_parks | Non puoi leggere le misure dell'impianto. | Controlla l'UID e il tuo ruolo sull'impianto. |
400 over_budget | L'esportazione è troppo grande. | Usa il passo o il periodo che la risposta nomina in hint, oppure dividi l'esportazione. |
400 step_not_served | Il modello non ha valori con questo passo. | Usa uno dei passi in hint.offered_common. |
429 | Troppe esportazioni contemporaneamente, oppure il limite orario è esaurito. | Attendi i secondi indicati nell'header Retry-After. |
| I caratteri speciali appaiono sbagliati | I file sono in UTF-8. | Indica la codifica quando importi il file. |
Esempio Python: generatore di report SolarViz
Lo script recupera le informazioni sull'impianto, gli eventi importanti del trimestre in corso e i valori mensili dell'anno fino a oggi, e costruisce un PDF con un riepilogo, un grafico e due tabelle.
Prerequisiti
pip install requests pandas matplotlib reportlab
requests— le richieste a Miroxpandas— lettura e somma dei dati CSVmatplotlib— il graficoreportlab— il file PDF
Lo script legge il token dalla variabile d'ambiente MIROX_TOKEN:
export MIROX_TOKEN="YOUR_API_TOKEN"
python external_report_generator.py
Codice di esempio completo
Puoi anche scaricare lo script.
#!/usr/bin/env python3
"""
SolarViz - Mirox External Report Generator
Fetches the data of one plant from the Mirox API and builds an energy
production report with a chart as a PDF file:
* plant information GET /api/v1/export/report/{plant}/info
* events GET /api/v1/export/report/{plant}/events.csv
* monthly production figures GET /api/v1/export/series/template/{template}
Requires: pip install requests pandas matplotlib reportlab
The API token is read from the environment variable MIROX_TOKEN.
"""
import io
import os
import sys
import time
from datetime import datetime, timezone
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import pandas as pd
import requests
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.platypus import Image, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
# Configuration - replace with your values
API_URL = "https://service.mirox.io"
API_TOKEN = os.environ.get("MIROX_TOKEN", "YOUR_API_TOKEN") # group "Metric Export"
PARK_UID = "ABC123DEF456" # the UID of the plant
TEMPLATE_UID = "ABCD12340002" # system template "Report Technical"
# The report covers this year so far; the events are those of the current quarter
YEAR = datetime.now(timezone.utc).year
QUARTER = (datetime.now(timezone.utc).month - 1) // 3 + 1
HEADERS = {"Authorization": f"Bearer {API_TOKEN}", "Accept-Encoding": "gzip"}
# One CSV dialect for every request, independent of regional settings
CSV_FORMAT = {"separator_csv": ",", "separator_decimal": "."}
# The columns the report reads, by metric id (header=id)
PRODUCTION = "plant.grid_feedin"
EXPECTED = "plant.expected_weather"
LOSS_GRID = "plant.shutdown_grid"
LOSS_MARKETER = "plant.shutdown_external"
PERFORMANCE_RATIO = "plant.pr_weather"
class ExportRefused(Exception):
"""A coded refusal of the Metric Export: what was wrong, and the way out."""
def __init__(self, status, detail):
self.status = status
self.code = detail.get("code", "")
self.params = detail.get("params", {})
self.hint = detail.get("hint", {})
super().__init__(f"{status} {self.code}: {detail.get('message', '')} "
f"params={self.params} hint={self.hint}")
def get(path, params=None, retries=3):
"""GET with the token. Waits and repeats when the export is busy (429)."""
for _attempt in range(retries):
response = requests.get(f"{API_URL}{path}", headers=HEADERS, params=params, timeout=90)
if response.status_code == 429:
time.sleep(int(response.headers.get("Retry-After", "5")))
continue
if response.status_code >= 400:
try:
detail = response.json().get("detail")
except ValueError:
detail = None
if isinstance(detail, dict):
raise ExportRefused(response.status_code, detail)
response.raise_for_status()
return response
raise RuntimeError(f"{path}: still busy after {retries} attempts")
def fetch_park_info():
"""Plant information as a dictionary."""
return get(f"/api/v1/export/report/{PARK_UID}/info").json()
def fetch_events(year, quarter):
"""The important events of a quarter as a DataFrame."""
response = get(f"/api/v1/export/report/{PARK_UID}/events.csv",
{"year": year, "quarter": quarter, **CSV_FORMAT})
events = pd.read_csv(io.StringIO(response.content.decode("utf-8")))
if events.empty:
return events
# A priority of 1000 or more marks an important event
events = events[events["Priority"] >= 1000]
return events.drop(columns=["Event ID", "Priority"], errors="ignore")
def fetch_metrics(year):
"""Monthly figures of the template for the year so far.
header=id gives column names that never change. A year in progress ends
now; the notes of the answer say so.
"""
response = get(f"/api/v1/export/series/template/{TEMPLATE_UID}", {
"park": PARK_UID, "year": year, "step": "month", "header": "id", **CSV_FORMAT})
for note in filter(None, response.headers.get("X-Mirox-Notes", "").split(";")):
print(f" note: {note}")
metrics = pd.read_csv(io.StringIO(response.content.decode("utf-8")))
# An empty cell means "no data"; it is read as NaN and left out of the sums
return metrics.rename(columns={"timestamp": "month"})
def create_production_chart(metrics):
"""Monthly production with the shutdown losses stacked, against the expectation."""
plt.figure(figsize=(8, 4))
positions = range(len(metrics))
production = metrics[PRODUCTION].fillna(0)
loss_grid = metrics[LOSS_GRID].fillna(0)
loss_marketer = metrics[LOSS_MARKETER].fillna(0)
plt.bar(positions, production, color="#2986cc", label="Grid feed-in")
plt.bar(positions, loss_grid, bottom=production, color="#e06666",
label="Shutdown loss, grid operator")
plt.bar(positions, loss_marketer, bottom=production + loss_grid, color="#f1c232",
label="Shutdown loss, direct marketer")
plt.plot(positions, metrics[EXPECTED], marker="o", color="#6aa84f", linewidth=2,
label="Expected production")
plt.title("Monthly production and shutdown losses")
plt.xlabel("Month")
plt.ylabel("Energy (kWh)")
plt.xticks(positions, metrics["month"], rotation=45)
plt.grid(axis="y", alpha=0.3)
plt.legend(loc="upper left")
plt.tight_layout()
buffer = io.BytesIO()
plt.savefig(buffer, format="png", dpi=300)
buffer.seek(0)
plt.close()
return buffer
def calculate_performance(metrics):
"""The figures of the summary table."""
production = metrics[PRODUCTION].sum()
expected = metrics[EXPECTED].sum()
loss_grid = metrics[LOSS_GRID].sum()
loss_marketer = metrics[LOSS_MARKETER].sum()
losses = loss_grid + loss_marketer
return {
"production": production,
"expected": expected,
"production_vs_expected": production / expected * 100 if expected else None,
"loss_grid": loss_grid,
"loss_marketer": loss_marketer,
"losses": losses,
"loss_share": losses / (production + losses) * 100 if production + losses else None,
}
def number(value, unit="", digits=2):
if value is None or pd.isna(value):
return "-"
return f"{value:,.{digits}f}{unit}"
def plain_table(rows, widths):
table = Table(rows, colWidths=widths)
table.setStyle(TableStyle([
("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 3),
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
]))
return table
def grid_table(rows, font_size=9):
table = Table(rows, repeatRows=1)
table.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("ALIGN", (0, 0), (-1, 0), "CENTER"),
("ALIGN", (1, 1), (-1, -1), "RIGHT"),
("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("FONTSIZE", (0, 0), (-1, -1), font_size),
]))
return table
def generate_pdf_report(park_info, events, metrics, performance, chart):
"""Build the PDF and return its file name."""
output_file = f"SolarViz_Report_{park_info['name'].replace(' ', '_')}_{YEAR}_Q{QUARTER}.pdf"
doc = SimpleDocTemplate(output_file, pagesize=A4)
styles = getSampleStyleSheet()
styles["Title"].fontSize = 18
address = park_info.get("address") or {}
portfolio = park_info.get("portfolio") or {}
organization = park_info.get("organization") or {}
elements = [
Paragraph(f"Energy Production Report: {park_info['name']}", styles["Title"]),
Spacer(1, 0.5 * cm),
Paragraph("Plant Information", styles["Heading2"]),
plain_table([
["Name:", park_info["name"]],
["Type:", park_info.get("type", "")],
["Location:", f"{address.get('city', '')}, {address.get('country', '')}"],
["Peak power:", number((park_info.get("peak_production_w") or 0) / 1e6, " MW")],
["Portfolio:", portfolio.get("name", "")],
["Organization:", organization.get("name", "")],
], [3 * cm, 10 * cm]),
Spacer(1, 0.5 * cm),
Paragraph(f"Summary ({YEAR} to date)", styles["Heading2"]),
plain_table([
["Grid feed-in:", number(performance["production"], " kWh")],
["Expected production:", number(performance["expected"], " kWh")],
["Feed-in against expectation:", number(performance["production_vs_expected"], " %")],
["Shutdown losses:", number(performance["losses"], " kWh")],
[" by the grid operator:", number(performance["loss_grid"], " kWh")],
[" by the direct marketer:", number(performance["loss_marketer"], " kWh")],
["Share of the losses:", number(performance["loss_share"], " %")],
], [6 * cm, 7 * cm]),
Spacer(1, 0.5 * cm),
Paragraph("Monthly Production", styles["Heading2"]),
Image(chart, width=16 * cm, height=8 * cm),
Spacer(1, 0.5 * cm),
]
rows = [["Month", "Feed-in (kWh)", "Expected (kWh)", "Loss grid (kWh)",
"Loss marketer (kWh)", "PR (%)"]]
for _, row in metrics.iterrows():
rows.append([row["month"], number(row[PRODUCTION]), number(row[EXPECTED]),
number(row[LOSS_GRID]), number(row[LOSS_MARKETER]),
number(row[PERFORMANCE_RATIO], digits=1)])
elements += [grid_table(rows), Spacer(1, 0.5 * cm),
Paragraph("Important Events", styles["Heading2"])]
if events.empty:
elements.append(Paragraph("No important events in this quarter.", styles["Normal"]))
else:
elements.append(grid_table([events.columns.tolist()] + events.values.tolist(), 8))
elements += [
Spacer(1, 1 * cm),
Paragraph("Data Source", styles["Heading2"]),
Paragraph(
"Data exported from the Mirox platform through the Metric Export API.<br/>"
f"Created: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M')} UTC<br/><br/>"
"<i>Generated by SolarViz - External Reporting Tool</i>", styles["Normal"]),
]
doc.build(elements)
return output_file
def main():
try:
print("Fetching plant information...")
park_info = fetch_park_info()
print("Fetching events...")
events = fetch_events(YEAR, QUARTER)
print("Fetching monthly figures...")
metrics = fetch_metrics(YEAR)
print("Building the report...")
performance = calculate_performance(metrics)
chart = create_production_chart(metrics)
output_file = generate_pdf_report(park_info, events, metrics, performance, chart)
print(f"Report created: {output_file}")
except ExportRefused as refused:
print(f"The export was refused: {refused}")
if refused.code == "over_budget":
print("Use a coarser step or a shorter range, see hint.step and hint.max_days.")
sys.exit(1)
except requests.RequestException as error:
print(f"The request failed: {error}")
sys.exit(1)
if __name__ == "__main__":
main()
Come funziona
- Configurazione. L'indirizzo di Mirox, l'impianto e il modello stanno in cima. Il token viene dall'ambiente, così non finisce mai in un file che condividi.
- Una sola funzione per tutte le richieste.
getinvia il token, attende e ripete quando Mirox risponde429, e trasforma un rifiuto dell'esportazione in un errore che portacode,paramsehint. - Valori mensili.
fetch_metricsrichiede il modello sull'anno constep=montheheader=ide stampa le note della risposta. In un anno ancora in corso, il periodo termina adesso. - Valori. Le celle vuote vengono lette come «nessun dato» ed escluse dalle somme.
- Report. ReportLab costruisce il PDF: informazioni sull'impianto, riepilogo, grafico, tabella mensile ed eventi importanti.
Personalizzazione
- Periodo: modifica
YEARper i valori eQUARTERper gli eventi. - Colonne: usa un altro modello, oppure richiedi le metriche direttamente con
GET /v1/export/seriesemetrics=…. - Dati più fini:
step=daydà righe giornaliere. Per curve a 15 minuti, richiedi le metriche di potenza conGET /v1/export/series. - Più impianti: passa più UID in
park, oppure unportfolio. - Automazione: esegui lo script da uno scheduler, con il token nell'ambiente del job.
Funzionalità correlate
- Metric Export per Excel — ricette e Power Query
- Generazione di report esterni — quando costruire i report fuori da Mirox
- Metric Export API — il riferimento delle richieste usate qui
- Utilizzo dei token API — crea e gestisci il token
- Report — i report che Mirox genera da sé