Gerador de relatórios externo
Este exemplo obtém do Mirox os dados de uma central e cria a partir deles um relatório próprio: primeiro os três pedidos com curl, depois um gerador de relatórios em Python completo que os transforma num ficheiro PDF.
Procura antes trabalho em folhas de cálculo?
Para o Excel — curvas de produção, várias centrais lado a lado, inversores individuais, sensores, turbinas — consulte Exportação de métricas para Excel.
Pré-requisitos
- Uma conta Mirox que possa ler a central.
- O UID da central — o identificador único do recurso de central sobre o qual quer criar o relatório.
- Um token de API do grupo Metric Export — consulte Utilização de tokens de API.
Os três pedidos
| Dados | Pedido | Resposta |
|---|---|---|
| Informações da central | GET /v1/export/report/{park_uid}/info | JSON |
| Eventos | GET /v1/export/report/{park_uid}/events.csv | CSV |
| Valores de produção | GET /v1/export/series/template/{template_uid} | CSV, ou JSON com format=json |
Todos os caminhos são relativos a https://service.mirox.io/api. Os parâmetros do terceiro pedido estão descritos na API de exportação de métricas, os dos dois primeiros na API de exportação legada.
Exemplo 1: informações da central
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"
}
}
Exemplo 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,quarteremonthselecionam o período. Sem eles, a exportação cobre o ano em curso.- Sem os dois parâmetros de separador, as colunas são separadas por
;e o separador decimal é,. - A data de fim de um evento ainda aberto é
Ongoing.
Filtrar eventos por prioridade
Uma Priority de 1000 ou mais assinala um evento importante, como um corte que custa produção ou um alarme. Os valores mais baixos são informativos, como uma ligação que foi restabelecida.
Exemplo 3: valores mensais de produção
O modelo do sistema Relatório técnico (ABCD12340002) para o primeiro trimestre, por mês, com IDs como cabeçalhos das colunas:
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á cabeçalhos de colunas que nunca mudam. Comheader=name, a predefinição, os cabeçalhos são os nomes das métricas no idioma que pedir, comoGrid feed-in (kWh).- Uma célula vazia significa que não há valor. Não significa zero.
- Várias centrais ou uma carteira inteira cabem num só pedido:
park=ABC123DEF456,DEF456ABC123ouportfolio=<uid>, commulti_park_agg=splitpara uma coluna por central. - As colunas do modelo estão listadas no guia de migração.
Muitas centrais num período longo
Uma central ao longo de um ano inteiro cabe num único pedido, e 25 centrais também. Um pedido demasiado grande responde 400 over_budget e indica o período mais longo que cabe. Peça então um trimestre de cada vez, ou um portefólio de cada vez, e junte os ficheiros. Consulte Dividir uma exportação grande.
O modelo anterior
Os relatórios assentes no modelo anterior Report Technical v1 continuam a funcionar. A sua ligação e as suas colunas não mudaram:
curl "https://service.mirox.io/api/v1/export/metrics/template/ABCD12340001?park=ABC123DEF456&resolution=monthly" \
-H "Authorization: Bearer YOUR_API_TOKEN"
O que difere no sucessor ABCD12340002:
ABCD12340001 (legado) | ABCD12340002 | |
|---|---|---|
| Ligação | /v1/export/metrics/template/… | /v1/export/series/template/… |
| Cabeçalhos das colunas | os nomes da exportação anterior, como Energy Production (kWh) | os nomes da coleção de métricas, como Grid feed-in (kWh), ou IDs com header=id |
| Primeira coluna | Date (YYYY-MM) | Date, ou timestamp com header=id |
| Coluna adicional | Days in Month (d) na resolução mensal | nenhuma |
| Valor em falta | 0 | vazio |
| Dias e meses | UTC | a hora local da central |
| Colunas calculadas | incluídas | o performance ratio, a produção esperada e a produção específica estão incluídos; as restantes ficam de fora |
Usar os pedidos noutras ferramentas
- Python: a biblioteca
requests, como no script abaixo. - Excel: Power Query com o cabeçalho
Authorization— consulte Excel com Power Query. - Power BI: a mesma ligação Power Query do Excel.
- Tableau e outras: qualquer ferramenta que leia uma ligação CSV ou JSON e possa enviar um cabeçalho.
Resolução de problemas
| Resposta | Significado | O que fazer |
|---|---|---|
401 | O token está em falta, expirou ou foi revogado. | Crie um novo token, ou rode o existente. |
403 no_accessible_parks | Não pode ler as medições da central. | Verifique o UID e a sua função na central. |
400 over_budget | A exportação é demasiado grande. | Use o passo ou o período que a resposta indica em hint, ou divida a exportação. |
400 step_not_served | O modelo não tem valor neste passo. | Use um dos passos em hint.offered_common. |
429 | Demasiadas exportações em simultâneo, ou o limite horário está esgotado. | Aguarde os segundos indicados no cabeçalho Retry-After. |
| Os caracteres especiais aparecem errados | Os ficheiros estão em UTF-8. | Indique a codificação quando importar o ficheiro. |
Exemplo em Python: gerador de relatórios SolarViz
O script obtém as informações da central, os eventos importantes do trimestre em curso e os valores mensais do ano até à data, e cria um PDF com um resumo, um gráfico e duas tabelas.
Pré-requisitos
pip install requests pandas matplotlib reportlab
requests— os pedidos ao Miroxpandas— ler e somar os dados CSVmatplotlib— o gráficoreportlab— o ficheiro PDF
O script lê o token da variável de ambiente MIROX_TOKEN:
export MIROX_TOKEN="YOUR_API_TOKEN"
python external_report_generator.py
Código de exemplo completo
Também pode transferir o 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()
Como funciona
- Configuração. O endereço do Mirox, a central e o modelo estão no topo. O token vem do ambiente, pelo que nunca vai parar a um ficheiro que partilhe.
- Uma função para todos os pedidos.
getenvia o token, aguarda e repete quando o Mirox responde429, e transforma uma recusa da exportação num erro que contémcode,paramsehint. - Valores mensais.
fetch_metricspede o modelo para o ano comstep=montheheader=id, e imprime as notas da resposta. Num ano ainda em curso, o período termina agora. - Valores. As células vazias são lidas como «sem dados» e ficam de fora das somas.
- Relatório. O ReportLab cria o PDF: informações da central, resumo, gráfico, tabela mensal e eventos importantes.
Personalização
- Período: altere
YEARpara os valores eQUARTERpara os eventos. - Colunas: use outro modelo, ou peça métricas diretamente com
GET /v1/export/seriesemetrics=…. - Dados mais finos:
step=daydá linhas diárias. Para curvas de 15 minutos, peça as métricas de potência comGET /v1/export/series. - Várias centrais: passe vários UIDs em
park, ou umportfolio. - Automatização: execute o script a partir de um agendador, com o token no ambiente da tarefa.
Funcionalidades relacionadas
- Exportação de métricas para Excel — receitas e Power Query
- Geração externa de relatórios — quando criar relatórios fora do Mirox
- API de exportação de métricas — a referência dos pedidos aqui usados
- Utilização de tokens de API — criar e gerir o token
- Relatórios — os relatórios que o próprio Mirox gera