Generador de informes externos
Este ejemplo obtiene de Mirox los datos de una planta y crea con ellos un informe propio: primero las tres solicitudes con curl, y después un generador de informes en Python completo que las convierte en un archivo PDF.
¿Buscas trabajar con hojas de cálculo?
Para Excel —curvas de producción, varias plantas una junto a otra, inversores, sensores o aerogeneradores individuales— consulta Metric Export para Excel.
Requisitos previos
- Una cuenta de Mirox que pueda leer la planta.
- El UID de la planta: el identificador único del recurso de planta sobre el que quieres informar.
- Un token de API del grupo Metric Export: consulta Uso de tokens de API.
Las tres solicitudes
| Datos | Solicitud | Respuesta |
|---|---|---|
| Información de la planta | GET /v1/export/report/{park_uid}/info | JSON |
| Eventos | GET /v1/export/report/{park_uid}/events.csv | CSV |
| Cifras de producción | GET /v1/export/series/template/{template_uid} | CSV, o JSON con format=json |
Todas las rutas siguen a https://service.mirox.io/api. Los parámetros de la tercera solicitud se describen en la API de Metric Export; los de las dos primeras, en la API de exportación heredada.
Ejemplo 1: información de la planta
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"
}
}
Ejemplo 2: eventos
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,quarterymonthseleccionan el periodo. Sin ellos, la exportación cubre el año en curso.- Sin los dos parámetros de separador, las columnas se separan con
;y el separador decimal es,. - La fecha de fin de un evento que sigue abierto es
Ongoing.
Filtra los eventos por prioridad
Una Priority de 1000 o más marca un evento importante, como una parada que cuesta producción o una alarma. Los valores más bajos son informativos, como una conexión que se ha restablecido.
Ejemplo 3: cifras de producción mensuales
La plantilla del sistema Informe técnico (ABCD12340002) para el primer trimestre, por mes, con ids como encabezados de columna:
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=idda encabezados de columna que nunca cambian. Conheader=name, el valor predeterminado, los encabezados son los nombres de las métricas en el idioma que pidas, comoGrid feed-in (kWh).- Una celda vacía significa que no hay valor. No significa cero.
- Varias plantas o un portfolio entero van en una sola solicitud:
park=ABC123DEF456,DEF456ABC123oportfolio=<uid>, conmulti_park_agg=splitpara una columna por planta. - Las columnas de la plantilla se enumeran en la guía de migración.
Muchas plantas en un periodo largo
Una planta durante un año entero cabe en una sola solicitud, y también 25 plantas. Una solicitud demasiado grande responde 400 over_budget y nombra el periodo más largo que cabe. Solicita entonces un trimestre cada vez, o un portfolio cada vez, y une los archivos. Consulta Dividir una exportación grande.
La plantilla anterior
Los informes basados en la plantilla anterior Report Technical v1 siguen funcionando. Su enlace y sus columnas no han cambiado:
curl "https://service.mirox.io/api/v1/export/metrics/template/ABCD12340001?park=ABC123DEF456&resolution=monthly" \
-H "Authorization: Bearer YOUR_API_TOKEN"
En qué se diferencia la sucesora ABCD12340002:
ABCD12340001 (heredada) | ABCD12340002 | |
|---|---|---|
| Enlace | /v1/export/metrics/template/… | /v1/export/series/template/… |
| Encabezados de columna | los nombres de la exportación anterior, como Energy Production (kWh) | los nombres de la colección de métricas, como Grid feed-in (kWh), o ids con header=id |
| Primera columna | Date (YYYY-MM) | Date, o timestamp con header=id |
| Columna adicional | Days in Month (d) con resolución mensual | ninguna |
| Valor ausente | 0 | vacío |
| Días y meses | UTC | la hora local de la planta |
| Columnas calculadas | incluidas | el performance ratio, la producción esperada y la producción específica están incluidos; las demás se omiten |
Usar las solicitudes en otras herramientas
- Python: la biblioteca
requests, como en el script de más abajo. - Excel: Power Query con el encabezado
Authorization: consulta Excel con Power Query. - Power BI: la misma conexión de Power Query que en Excel.
- Tableau y otras: cualquier herramienta que lea un enlace CSV o JSON y pueda enviar un encabezado.
Solución de problemas
| Respuesta | Significado | Qué hacer |
|---|---|---|
401 | El token falta, ha caducado o se ha revocado. | Crea un token nuevo, o rota el existente. |
403 no_accessible_parks | No puedes leer las mediciones de la planta. | Comprueba el UID y tu rol en la planta. |
400 over_budget | La exportación es demasiado grande. | Usa el paso o el periodo que la respuesta nombra en hint, o divide la exportación. |
400 step_not_served | La plantilla no tiene valor con este paso. | Usa uno de los pasos de hint.offered_common. |
429 | Demasiadas exportaciones a la vez, o el cupo por hora está agotado. | Espera los segundos del encabezado Retry-After. |
| Los caracteres especiales se ven mal | Los archivos son UTF-8. | Indica la codificación al importar el archivo. |
Ejemplo en Python: generador de informes SolarViz
El script obtiene la información de la planta, los eventos importantes del trimestre en curso y las cifras mensuales del año hasta la fecha, y crea un PDF con un resumen, un gráfico y dos tablas.
Requisitos previos
pip install requests pandas matplotlib reportlab
requests: las solicitudes a Miroxpandas: leer y sumar los datos CSVmatplotlib: el gráficoreportlab: el archivo PDF
El script lee el token de la variable de entorno MIROX_TOKEN:
export MIROX_TOKEN="YOUR_API_TOKEN"
python external_report_generator.py
Código de ejemplo completo
También puedes descargar el 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()
Cómo funciona
- Configuración. La dirección de Mirox, la planta y la plantilla están al principio. El token viene del entorno, así que nunca acaba en un archivo que compartas.
- Una función para todas las solicitudes.
getenvía el token, espera y repite cuando Mirox responde429, y convierte un rechazo de la exportación en un error que llevacode,paramsyhint. - Cifras mensuales.
fetch_metricspide la plantilla para todo el año constep=monthyheader=id, e imprime las notas de la respuesta. En un año que aún está en curso, el periodo termina ahora. - Cifras. Las celdas vacías se leen como «sin datos» y se dejan fuera de las sumas.
- Informe. ReportLab crea el PDF: información de la planta, resumen, gráfico, tabla mensual y eventos importantes.
Personalización
- Periodo: cambia
YEARpara las cifras yQUARTERpara los eventos. - Columnas: usa otra plantilla, o solicita métricas directamente con
GET /v1/export/seriesymetrics=…. - Datos más finos:
step=dayda filas diarias. Para curvas de 15 minutos, solicita las métricas de potencia conGET /v1/export/series. - Varias plantas: pasa varios UIDs en
park, o unportfolio. - Automatización: ejecuta el script desde un planificador, con el token en el entorno de la tarea.
Funciones relacionadas
- Metric Export para Excel: recetas y Power Query
- Generación de informes externos: cuándo crear informes fuera de Mirox
- API de Metric Export: la referencia de las solicitudes usadas aquí
- Uso de tokens de API: crea y gestiona el token
- Informes: los informes que genera el propio Mirox