Loading…
Loading…
Reading guide
Everything you see on this website is a real, untouched measurement from the plant. This guide explains where the data comes from, how far it lags behind and how to interpret it — whether you are just curious or after the technical detail.
Every number you see here has made the same trip, from the plant to your screen:
4 strings + AC meter
The meters record current, voltage, power and energy once per minute: 4 photovoltaic strings on the DC side and one meter at the inverter output (AC).
UWP 4.0 · Modbus
A device installed at the plant itself polls the meters over an industrial bus (Modbus RTU) and stores every reading.
SFTP · 1 file/day
Shortly after midnight, the datalogger generates a file with all of the previous day's measurements and uploads it, encrypted (SFTP), to a UPM server.
PostgreSQL · every 6 h
An automated job checks every 6 hours for new files and loads them into the database. Reprocessing a file never duplicates a measurement.
API + charts
The pages query that database and show the values as they are: if a measurement does not exist, you will see a gap, never a made-up zero.
The most recent thing you will see here is yesterday's production — and the website always tells you when each piece of data is from.
The data travels in a daily batch: the file generated today carries yesterday's measurements, and the website ingests it over the following hours. In practice, the latest available sample is between one and roughly 30 hours old.
The website never hides this: the dashboard always states when the latest sample was taken ("Data up to…") and talks about the "last day with data", never about "today". A few hours of lag take nothing away from the data: to understand how much the plant produces, the full series matters, not the current minute.
Five terms are enough to read any chart on this website:
A gap in a chart means "no measurement", not "no production". If the datalogger goes down or a file never arrives, the measurements for that stretch simply do not exist — and the website shows them as absence. Turning a gap into a zero would be lying: it would claim the plant produced nothing when what actually happened is that nobody was measuring.
This is not hypothetical: between 26 and 28 June 2026 the plant's datalogger was down, and those days show up without a bar in the charts. That is exactly how it should look.
Next to the daily totals you will see the "n/4" completeness: how many of the 4 strings reported that day. A 3/4 day is not directly comparable to a 4/4 one — a whole string is missing — which is why the website flags it instead of hiding it.
Each string has a cumulative energy counter (kWh) that only goes up, like a car's odometer. A day's energy is the difference in that counter between the end of the day and its start: if a counter read 1,900 kWh at midnight and 1,925 kWh the following midnight, that string would have generated 25 kWh.
This method holds up well under failures: even if intermediate samples are missing, the counter "remembers" the energy generated in between. The actual computation works on differences between consecutive samples so it can detect counter resets or overflows, and with fewer than two valid readings the day is left as "no data", never a false zero. Plant energy is the sum of the 4 strings; if any string contributes nothing, the total is flagged as partial (n/4).
Everything this website shows can also be queried by machine: a public read-only REST API, with no registration or key, JSON responses and open CORS (requests from any origin). It serves the same honest data as the charts: daily batches, gaps as null, and n/4 completeness always visible.
GET /api/monitoring/latestLatest valid reading per variable (DC current, power, accumulated energy and voltage).
| Parameter | Values | Description |
|---|---|---|
| scope | VMU-S_1 … VMU-S_4 · plant | Scope: one specific DC string or the plant aggregate. Omitted, VMU-S_1 is served. |
GET /api/monitoring/seriesTime series of one variable, resampled to the requested step. At most 366 days and 1,500 points per response.
| Parameter | Values | Description |
|---|---|---|
| var | A DC · kW DC · kWh (+)DC · V DC | Required. Variable to query. "V DC" does not allow scope=plant: voltage does not add up across strings (400). |
| scope | VMU-S_1 … VMU-S_4 · plant | Scope: one specific DC string or the plant aggregate. Omitted, VMU-S_1 is served. |
| from | ISO 8601 | Window start (ISO 8601). Defaults to one hour before "to". |
| to | ISO 8601 | Window end (ISO 8601). Defaults to the request time. If the requested window falls beyond the last available data, the served one is anchored to it keeping the duration (windowFrom/windowTo in the response). |
| step | ≥ 1 | Resampling step in minutes (minimum 1). Omitted, it is picked from the range: 1, 15, 60 or 1440 min. |
GET /api/monitoring/dailyDaily energy (kWh/day) with a per-DC-string breakdown, over a window ending on the last day with data. It serves the plant aggregate by default; you can also ask for one specific string.
| Parameter | Values | Description |
|---|---|---|
| scope | VMU-S_1 … VMU-S_4 · plant | Scope: one specific DC string or the plant aggregate. Omitted, the plant is served (unlike latest and series, which serve VMU-S_1). With a single string, the window ends on the last day with data OF THAT string and the breakdown carries a single entry. |
| days | 1 – 366 | Days in the window (default 7), counted backwards from the last day with data. |
| from | YYYY-MM-DD | Window start as a day (YYYY-MM-DD, Madrid time, inclusive). Sent together with "to", it overrides "days"; at most 366 days. The range is served as asked: days beyond the last one with data come back as null, the window is not trimmed. |
| to | YYYY-MM-DD | Window end as a day (YYYY-MM-DD, Madrid time, inclusive). Sent together with "from" and never earlier than it. |
| format | json · csv | Response format: json (default) or csv. The CSV downloads the days of the window (day, energy_kwh, strings_with_data, strings_total, complete) with a dot decimal separator; a day without data is an EMPTY cell, never a 0. Errors are always returned as JSON. |
Each CSV row is one day of the served window, in Madrid time. The filename carries the scope and the range actually served (solar-demo-center_daily-energy_<scope>_<from>_<to>.csv), so two downloads of different scopes never clash in your downloads folder.
| Column | Description |
|---|---|
| day | The day the row belongs to (YYYY-MM-DD, Madrid time). |
| energy_kwh | Energy generated that day, in kWh. An empty cell means there was no measurement that day; a 0 is a real measurement of zero. |
| strings_with_data | How many strings contributed data that day. Fewer than the total means the row's kWh is a partial sum: part of the facility is missing, it did not produce less. |
| strings_total | How many strings the requested scope covers: every string of the plant if you ask for the aggregate, just one if you asked for a specific string. |
| complete | true only when every string of the scope contributed that day. With a single string the total is 1, so that one contributing is enough. |
Latest reading of the plant aggregate, with curl:
curl "https://proyecto-solar.rlorenzo.com/api/monitoring/latest?scope=plant"Real response (abbreviated):
{
"updatedAt": "2026-07-08T22:00:00.000Z",
"values": { "A DC": 0, "kW DC": 0, "kWh (+)DC": 9972.1, "V DC": null },
"source": "postgres",
"scope": "plant",
"stringsWithData": { "A DC": 4, "kW DC": 4, "kWh (+)DC": 4 },
"stringsTotal": 4
}Open GET from any origin (Access-Control-Allow-Origin: *), with no key and no enforced rate limit. In exchange, fair use: the data changes once a day and the server caches for 15 minutes, so polling faster gets you nothing fresher.
If you reuse this data, cite the source: "UPM/Trina — Solar Demo Center". It is published as is, for demonstration, teaching and research purposes; the reuse terms are still to be agreed with the IES, who owns the data. The disclaimer is in the legal notice.
The full contract (schemas, parameters, responses and errors) is described in a machine-readable OpenAPI 3.1 specification, ready for Swagger UI, Postman or client generation:
/api/openapi.jsonYou can now interpret any chart on the dashboard — gaps included.
Go to the dashboard