Externer Berichtsgenerator
Dieses Beispiel holt die Daten einer Anlage aus Mirox und erstellt daraus einen eigenen Bericht: zuerst die drei Anfragen mit curl, danach ein vollständiger Python-Berichtsgenerator, der daraus eine PDF-Datei macht.
Suchen Sie stattdessen Arbeit mit Tabellen?
Für Excel — Produktionskurven, mehrere Anlagen nebeneinander, einzelne Wechselrichter, Sensoren, Turbinen — siehe Metric Export für Excel.
Voraussetzungen
- Ein Mirox-Konto, das die Anlage lesen darf.
- Die UID der Anlage — die eindeutige Kennung der Anlagen-Ressource, über die Sie berichten möchten.
- Ein API-Token der Gruppe Metric Export — siehe API-Token-Nutzung.
Die drei Anfragen
| Daten | Anfrage | Antwort |
|---|---|---|
| Anlageninformationen | GET /v1/export/report/{park_uid}/info | JSON |
| Ereignisse | GET /v1/export/report/{park_uid}/events.csv | CSV |
| Produktionswerte | GET /v1/export/series/template/{template_uid} | CSV, oder JSON mit format=json |
Alle Pfade folgen auf https://service.mirox.io/api. Die Parameter der dritten Anfrage beschreibt die Metric-Export-API, die der ersten beiden die Legacy-Export-API.
Beispiel 1: Anlageninformationen
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"
}
}
Beispiel 2: Ereignisse
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,quarterundmonthwählen den Zeitraum. Ohne sie umfasst der Export das laufende Jahr.- Ohne die beiden Trennzeichen-Parameter sind die Spalten durch
;getrennt, und das Dezimalzeichen ist,. - Das Enddatum eines noch offenen Ereignisses ist
Ongoing.
Ereignisse nach Priorität filtern
Eine Priority von 1000 oder mehr kennzeichnet ein wichtiges Ereignis, etwa eine Abschaltung, die Produktion kostet, oder einen Alarm. Niedrigere Werte sind informativ, etwa eine Verbindung, die zurückgekehrt ist.
Beispiel 3: Monatliche Produktionswerte
Die Systemvorlage Technischer Bericht (ABCD12340002) für das erste Quartal, je Monat, mit IDs als Spaltenköpfen:
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=idergibt Spaltenköpfe, die sich nie ändern. Mitheader=name, dem Standard, sind die Spaltenköpfe die Namen der Metriken in der Sprache, die Sie anfordern, etwaGrid feed-in (kWh).- Eine leere Zelle bedeutet, dass es keinen Wert gibt. Sie bedeutet nicht null.
- Mehrere Anlagen oder ein ganzes Portfolio passen in eine Anfrage:
park=ABC123DEF456,DEF456ABC123oderportfolio=<uid>, mitmulti_park_agg=splitfür eine Spalte je Anlage. - Die Spalten der Vorlage stehen im Leitfaden zum Umstieg.
Viele Anlagen über einen langen Zeitraum
Eine Anlage über ein ganzes Jahr passt in eine Anfrage, ebenso 25 Anlagen. Eine zu große Anfrage antwortet mit 400 over_budget und nennt den längsten Zeitraum, der passt. Fordern Sie dann ein Quartal nach dem anderen oder ein Portfolio nach dem anderen an und fügen Sie die Dateien zusammen. Siehe Einen großen Export aufteilen.
Die bisherige Vorlage
Berichte, die auf der bisherigen Vorlage Report Technical v1 aufbauen, funktionieren weiter. Ihr Link und ihre Spalten sind unverändert:
curl "https://service.mirox.io/api/v1/export/metrics/template/ABCD12340001?park=ABC123DEF456&resolution=monthly" \
-H "Authorization: Bearer YOUR_API_TOKEN"
Was sich beim Nachfolger ABCD12340002 unterscheidet:
ABCD12340001 (Legacy) | ABCD12340002 | |
|---|---|---|
| Link | /v1/export/metrics/template/… | /v1/export/series/template/… |
| Spaltenköpfe | die Namen des bisherigen Exports, etwa Energy Production (kWh) | die Namen der Metriksammlung, etwa Grid feed-in (kWh), oder IDs mit header=id |
| Erste Spalte | Date (YYYY-MM) | Date, oder timestamp mit header=id |
| Zusätzliche Spalte | Days in Month (d) bei monatlicher Auflösung | keine |
| Fehlender Wert | 0 | leer |
| Tage und Monate | UTC | die Ortszeit der Anlage |
| Berechnete Spalten | enthalten | die Performance Ratio, die erwartete Erzeugung und der spezifische Ertrag sind enthalten; die übrigen werden ausgelassen |
Die Anfragen in anderen Werkzeugen nutzen
- Python: die Bibliothek
requests, wie im Skript unten. - Excel: Power Query mit dem Header
Authorization— siehe Excel mit Power Query. - Power BI: dieselbe Power-Query-Verbindung wie in Excel.
- Tableau und andere: jedes Werkzeug, das einen CSV- oder JSON-Link liest und einen Header senden kann.
Fehlerbehebung
| Antwort | Bedeutung | Was zu tun ist |
|---|---|---|
401 | Das Token fehlt, ist abgelaufen oder widerrufen. | Erstellen Sie ein neues Token oder rotieren Sie das bestehende. |
403 no_accessible_parks | Sie können die Messwerte der Anlage nicht lesen. | Prüfen Sie die UID und Ihre Rolle auf der Anlage. |
400 over_budget | Der Export ist zu groß. | Nutzen Sie die Schrittweite oder den Zeitraum, den die Antwort in hint nennt, oder teilen Sie den Export auf. |
400 step_not_served | Die Vorlage hat in dieser Schrittweite keinen Wert. | Nutzen Sie eine der Schrittweiten in hint.offered_common. |
429 | Zu viele Exporte gleichzeitig, oder das stündliche Kontingent ist aufgebraucht. | Warten Sie die Sekunden im Header Retry-After ab. |
| Sonderzeichen sehen falsch aus | Die Dateien sind UTF-8. | Geben Sie die Kodierung an, wenn Sie die Datei importieren. |
Python-Beispiel: SolarViz Report Generator
Das Skript holt die Anlageninformationen, die wichtigen Ereignisse des laufenden Quartals und die Monatswerte des bisherigen Jahres und erstellt eine PDF-Datei mit einer Zusammenfassung, einem Diagramm und zwei Tabellen.
Voraussetzungen
pip install requests pandas matplotlib reportlab
requests— die Anfragen an Miroxpandas— das Lesen und Aufsummieren der CSV-Datenmatplotlib— das Diagrammreportlab— die PDF-Datei
Das Skript liest das Token aus der Umgebungsvariablen MIROX_TOKEN:
export MIROX_TOKEN="YOUR_API_TOKEN"
python external_report_generator.py
Vollständiger Beispielcode
Sie können das Skript auch herunterladen.
#!/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()
So funktioniert es
- Konfiguration. Die Adresse von Mirox, die Anlage und die Vorlage stehen oben. Das Token kommt aus der Umgebung, es landet also nie in einer Datei, die Sie weitergeben.
- Eine Funktion für alle Anfragen.
getsendet das Token, wartet und wiederholt, wenn Mirox mit429antwortet, und macht aus einer Ablehnung des Exports einen Fehler, dercode,paramsundhintträgt. - Monatswerte.
fetch_metricsfordert die Vorlage über das Jahr mitstep=monthundheader=idan und gibt die Hinweise der Antwort aus. In einem noch laufenden Jahr endet der Zeitraum jetzt. - Werte. Leere Zellen werden als „keine Daten“ gelesen und aus den Summen ausgelassen.
- Bericht. ReportLab erstellt die PDF-Datei: Anlageninformationen, Zusammenfassung, Diagramm, Monatstabelle und wichtige Ereignisse.
Anpassung
- Zeitraum: Ändern Sie
YEARfür die Werte undQUARTERfür die Ereignisse. - Spalten: Nutzen Sie eine andere Vorlage oder fordern Sie Metriken direkt mit
GET /v1/export/seriesundmetrics=…an. - Feinere Daten:
step=dayergibt Tageszeilen. Für 15-Minuten-Kurven fordern Sie die Leistungs-Metriken mitGET /v1/export/seriesan. - Mehrere Anlagen: Übergeben Sie mehrere UIDs in
parkoder einportfolio. - Automatisierung: Starten Sie das Skript über einen Scheduler, mit dem Token in der Umgebung des Jobs.
Verwandte Funktionen
- Metric Export für Excel — Rezepte und Power Query
- Externe Berichtserstellung — wann Sie Berichte außerhalb von Mirox erstellen
- Metric-Export-API — die Referenz der hier verwendeten Anfragen
- API-Token-Nutzung — das Token erstellen und verwalten
- Berichte — die Berichte, die Mirox selbst erstellt