Générateur de rapports externe
Cet exemple récupère les données d'une centrale depuis Mirox et en tire un rapport qui lui est propre : d'abord les trois requêtes avec curl, puis un générateur de rapports Python complet qui les transforme en fichier PDF.
Vous cherchez plutôt à travailler dans un tableur ?
Pour Excel — courbes de production, plusieurs centrales côte à côte, onduleurs individuels, capteurs, éoliennes — voir Metric Export pour Excel.
Prérequis
- Un compte Mirox qui peut lire la centrale.
- L'UID de la centrale — l'identifiant unique de la ressource centrale sur laquelle vous voulez produire un rapport.
- Un jeton d'API du groupe Metric Export — voir Utilisation des jetons d'API.
Les trois requêtes
| Données | Requête | Réponse |
|---|---|---|
| Informations de centrale | GET /v1/export/report/{park_uid}/info | JSON |
| Événements | GET /v1/export/report/{park_uid}/events.csv | CSV |
| Chiffres de production | GET /v1/export/series/template/{template_uid} | CSV, ou JSON avec format=json |
Tous les chemins suivent https://service.mirox.io/api. Les paramètres de la troisième requête sont décrits dans l'API Metric Export, ceux des deux premières dans l'API de l'ancien export.
Exemple 1 : informations de centrale
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"
}
}
Exemple 2 : événements
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,quarteretmonthsélectionnent la période. Sans eux, l'export couvre l'année en cours.- Sans les deux paramètres de séparateur, les colonnes sont séparées par
;et la marque décimale est,. - La date de fin d'un événement encore ouvert est
Ongoing.
Filtrer les événements par priorité
Une Priority de 1000 ou plus marque un événement important, comme une coupure qui coûte de la production ou une alarme. Les valeurs inférieures sont informatives, comme une connexion rétablie.
Exemple 3 : chiffres de production mensuels
Le modèle système Rapport technique (ABCD12340002) pour le premier trimestre, par mois, avec les identifiants comme en-têtes de colonne :
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=iddonne des en-têtes de colonne qui ne changent jamais. Avecheader=name, la valeur par défaut, les en-têtes sont les noms des indicateurs dans la langue que vous demandez, commeGrid feed-in (kWh).- Une cellule vide signifie qu'il n'y a pas de valeur. Elle ne signifie pas zéro.
- Plusieurs centrales ou un portefeuille entier tiennent dans une seule requête :
park=ABC123DEF456,DEF456ABC123ouportfolio=<uid>, avecmulti_park_agg=splitpour une colonne par centrale. - Les colonnes du modèle sont listées dans le guide de migration.
De nombreuses centrales sur une longue période
Une centrale sur une année entière tient dans une seule requête, et 25 centrales aussi. Une requête trop volumineuse répond 400 over_budget et nomme la période la plus longue qui convient. Demandez alors un trimestre à la fois, ou un portefeuille à la fois, et assemblez les fichiers. Voir Scinder un export volumineux.
Le modèle précédent
Les rapports construits sur le modèle précédent Report Technical v1 continuent de fonctionner. Son lien et ses colonnes sont inchangés :
curl "https://service.mirox.io/api/v1/export/metrics/template/ABCD12340001?park=ABC123DEF456&resolution=monthly" \
-H "Authorization: Bearer YOUR_API_TOKEN"
Ce qui diffère dans le successeur ABCD12340002 :
ABCD12340001 (ancien) | ABCD12340002 | |
|---|---|---|
| Lien | /v1/export/metrics/template/… | /v1/export/series/template/… |
| En-têtes de colonne | les noms de l'export précédent, comme Energy Production (kWh) | les noms de la collection d'indicateurs, comme Grid feed-in (kWh), ou les identifiants avec header=id |
| Première colonne | Date (YYYY-MM) | Date, ou timestamp avec header=id |
| Colonne supplémentaire | Days in Month (d) en résolution mensuelle | aucune |
| Valeur manquante | 0 | vide |
| Jours et mois | UTC | l'heure locale de la centrale |
| Colonnes calculées | incluses | le ratio de performance, la production attendue et le productible spécifique sont inclus ; les autres sont laissées de côté |
Utiliser les requêtes dans d'autres outils
- Python : la bibliothèque
requests, comme dans le script ci-dessous. - Excel : Power Query avec l'en-tête
Authorization— voir Excel avec Power Query. - Power BI : la même connexion Power Query que dans Excel.
- Tableau et autres : tout outil qui lit un lien CSV ou JSON et peut envoyer un en-tête.
Dépannage
| Réponse | Signification | Que faire |
|---|---|---|
401 | Le jeton est absent, expiré ou révoqué. | Créez un nouveau jeton, ou renouvelez le jeton existant. |
403 no_accessible_parks | Vous ne pouvez pas lire les mesures de la centrale. | Vérifiez l'UID et votre rôle sur la centrale. |
400 over_budget | L'export est trop volumineux. | Utilisez le pas ou la période que la réponse nomme dans hint, ou scindez l'export. |
400 step_not_served | Le modèle n'a pas de valeur à ce pas. | Utilisez l'un des pas de hint.offered_common. |
429 | Trop d'exports à la fois, ou le quota horaire est épuisé. | Attendez le nombre de secondes indiqué dans l'en-tête Retry-After. |
| Les caractères spéciaux s'affichent mal | Les fichiers sont en UTF-8. | Indiquez l'encodage lorsque vous importez le fichier. |
Exemple Python : générateur de rapports SolarViz
Le script récupère les informations de centrale, les événements importants du trimestre en cours et les chiffres mensuels de l'année écoulée jusqu'ici, puis construit un PDF avec une synthèse, un graphique et deux tableaux.
Prérequis
pip install requests pandas matplotlib reportlab
requests— les requêtes vers Miroxpandas— la lecture et l'addition des données CSVmatplotlib— le graphiquereportlab— le fichier PDF
Le script lit le jeton dans la variable d'environnement MIROX_TOKEN :
export MIROX_TOKEN="YOUR_API_TOKEN"
python external_report_generator.py
Code complet de l'exemple
Vous pouvez aussi télécharger le 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()
Comment il fonctionne
- Configuration. L'adresse de Mirox, la centrale et le modèle figurent en haut. Le jeton vient de l'environnement : il ne se retrouve donc jamais dans un fichier que vous partagez.
- Une seule fonction pour toutes les requêtes.
getenvoie le jeton, attend et recommence lorsque Mirox répond429, et transforme un refus de l'export en une erreur qui portecode,paramsethint. - Chiffres mensuels.
fetch_metricsdemande le modèle sur l'année avecstep=monthetheader=id, et affiche les remarques de la réponse. Dans une année encore en cours, la période se termine maintenant. - Chiffres. Les cellules vides sont lues comme « pas de données » et laissées hors des sommes.
- Rapport. ReportLab construit le PDF : informations de centrale, synthèse, graphique, tableau mensuel et événements importants.
Personnalisation
- Période : modifiez
YEARpour les chiffres etQUARTERpour les événements. - Colonnes : utilisez un autre modèle, ou demandez directement des indicateurs avec
GET /v1/export/seriesetmetrics=…. - Données plus fines :
step=daydonne des lignes journalières. Pour des courbes à 15 minutes, demandez les indicateurs de puissance avecGET /v1/export/series. - Plusieurs centrales : passez plusieurs UID dans
park, ou unportfolio. - Automatisation : exécutez le script depuis un planificateur, avec le jeton dans l'environnement de la tâche.
Fonctionnalités associées
- Metric Export pour Excel — recettes et Power Query
- Génération de rapports externes — quand créer des rapports en dehors de Mirox
- API Metric Export — la référence des requêtes utilisées ici
- Utilisation des jetons d'API — créez et gérez le jeton
- Rapports — les rapports que Mirox génère lui-même