OpenUV Index
The node queries the OpenUV.io API for the current UV index, ozone value and safe exposure times for one or more locations. Optionally it also delivers the hourly UV forecast of the day. Results are emitted as NGSI-LD style objects of type UVIndexObservation and UVIndexForecast.
- Node type
- openuv
- Package
- @htag/node-red-contrib-hypertegrity-weather
- Inputs / outputs
- 1 / 2
- Data source
- OpenUV.io API (api.openuv.io/api/v1)
When to use this node
The UV index describes the sunburn-effective radiation and is an important indicator for health protection in public spaces. OpenUV.io provides this value in real time for any coordinates, calculated from satellite data on the ozone layer. The OpenUV Index node integrates the service into Node-RED and handles authentication, the query per location and the transformation into a uniform data model.
Typical use cases are:
- UV advice for outdoor pools, schoolyards and playgrounds on display boards or in a citizen app.
- Municipal health dashboards with UV index, ozone and safe exposure times per skin type.
- Daily UV profile as a forecast for event planning or irrigation control.
How the node works
Trigger
An incoming message starts the fetch. The node never polls OpenUV.io on its own.
Query
For every location the /uv endpoint is called with latitude and longitude; with forecast enabled, /forecast is called as well.
Transform
The response is mapped to a UVIndexObservation object; every forecast point becomes its own UVIndexForecast object.
Emit
Output 1 receives one observation per location, output 2 one message per forecast hour.
Result: UV observations and forecasts with geoposition that can be passed directly to the NGSI-LD Builder.
The node is trigger-based. Combine it with an Inject node that fires at a fixed interval. Keep the quota of your OpenUV.io plan in mind: every trigger causes one request per location and, with forecast enabled, one more. Locations are queried one after another; the timeout per request is 10 seconds.
Configuration
The edit dialog consists of the Name field and two tabs.
Tab "Locations"
| Field | Type | Default | Meaning |
|---|---|---|---|
| Name | Text | empty | Display name of the node in the flow. |
| Map selection | Map | – | A click on the map places a point; "Add Selected Point" adds it to the list. Coordinates already present are rejected with "This location has already been added". |
Selected locations (locations) | List | empty | Entries with location name (name), latitude (lat) and longitude (lon). The name becomes part of the entity ID and appears in locationName. |
Tab "Settings"
| Field | Type | Default | Meaning |
|---|---|---|---|
API Key (apiKey) | Password field, required | empty | Your OpenUV.io API key. It is sent as HTTP header x-access-token. |
Forecast (outputForecast) | Checkbox | disabled | Include forecast data in output: additionally queries the /forecast endpoint and serves output 2. |
Obtaining an API key
Register at openuv.io and generate an API key in the dashboard. OpenUV.io offers a free plan with a limited daily quota as well as paid plans with higher limits; the current conditions are listed on the provider's website.
Input
The input serves exclusively as trigger. The node evaluates no property of the incoming message and does not copy it to the output.
| Property | Type | Meaning |
|---|---|---|
msg | any | Every message starts the fetch for all configured locations. |
Output
The node has two outputs. Both payloads are largely flat objects; only location is already structured as an NGSI-LD GeoProperty. They can be handed to the NGSI-LD Builder.
Output 1: UVIndexObservation
One message per location with msg.topic = "uv-observation" and msg.location = location name.
{
"id": "urn:ngsi-ld:UVIndexObservation:Outdoor_Pool_Example_Town",
"type": "UVIndexObservation",
"uVIndex": 3.2,
"uVIndexMax": 4.1,
"uVIndexMaxTime": "2026-10-01T11:20:00.000Z",
"ozone": 298.5,
"ozoneTime": "2026-10-01T07:00:00.000Z",
"safeExposure": {"st1": 52, "st2": 62, "st3": 83, "st4": 104, "st5": 166, "st6": 311},
"sunInfo": {
"sun_times": {"sunrise": "2026-10-01T05:25:00.000Z", "sunset": "2026-10-01T16:58:00.000Z"},
"sun_position": {"azimuth": 0.42, "altitude": 0.61}
},
"location": {"type": "GeoProperty", "value": {"type": "Point", "coordinates": [7.77, 49.44]}},
"locationName": "Outdoor Pool Example Town"
}
| Field | Meaning |
|---|---|
id | urn:ngsi-ld:UVIndexObservation: plus location name; all characters except letters and digits are replaced by _. |
type | Always UVIndexObservation. |
uVIndex | Current UV index (dimensionless, OpenUV uv). |
uVIndexMax | Daily maximum of the UV index (OpenUV uv_max). |
uVIndexMaxTime | Time of the daily maximum (OpenUV uv_max_time, ISO 8601). |
ozone | Ozone column in Dobson units (OpenUV ozone). |
ozoneTime | Timestamp of the ozone measurement (OpenUV ozone_time). |
safeExposure | Safe exposure time in minutes per Fitzpatrick skin type st1 to st6 (OpenUV safe_exposure_time), passed through unchanged. |
sunInfo | Sunrise and sunset times and sun position (OpenUV sun_info), passed through unchanged. |
location | GeoProperty with GeoJSON point [longitude, latitude] of the location. |
locationName | The location name you assigned. |
Output 2: UVIndexForecast
Only with forecast enabled. One message per forecast point (hourly for the current day) with msg.topic = "uv-forecast" and msg.location = location name.
{
"id": "urn:ngsi-ld:UVIndexForecast:Outdoor_Pool_Example_Town",
"type": "UVIndexForecast",
"dateIssued": "2026-10-01T12:00:00.000Z",
"uVIndex": 3.8,
"sunPosition": {"azimuth": 0.55, "altitude": 0.58},
"location": {"type": "GeoProperty", "value": {"type": "Point", "coordinates": [7.77, 49.44]}},
"locationName": "Outdoor Pool Example Town"
}
| Field | Meaning |
|---|---|
id | urn:ngsi-ld:UVIndexForecast: plus sanitised location name. |
type | Always UVIndexForecast. |
dateIssued | Time the forecast value applies to (OpenUV uv_time). |
uVIndex | Forecast UV index. |
sunPosition | Sun position at the forecast time (OpenUV sun_position). |
location, locationName | As on output 1. |
Example flow
The package ships with an example flow. Import it in Node-RED via Menu → Import → Examples → @htag/node-red-contrib-hypertegrity-weather → openuv-io. Alternatively download openuv-io.json and import it via Menu → Import.
Inject
An Inject node triggers the fetch manually or at a fixed interval.
OpenUV Index
The node queries OpenUV.io for the configured locations. In the example, API key and location list are empty and have to be completed.
Debug
Two Debug nodes show observation and forecast in the sidebar.
Show example flow as JSON
[
{"id": "tab1", "type": "tab", "label": "openuv-io", "disabled": false, "info": ""},
{"id": "inject1", "type": "inject", "z": "tab1", "name": "Go!", "props": [{"p": "payload"}, {"p": "topic", "vt": "str"}], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 110, "y": 100, "wires": [["uv1"]]},
{"id": "uv1", "type": "openuv", "z": "tab1", "name": "", "apiKey": "", "locations": [], "outputForecast": false, "x": 300, "y": 100, "wires": [["debug1"], ["debug2"]]},
{"id": "debug1", "type": "debug", "z": "tab1", "name": "Observation", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "x": 490, "y": 80, "wires": []},
{"id": "debug2", "type": "debug", "z": "tab1", "name": "Forecast", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "x": 480, "y": 120, "wires": []}
]
Status and errors
| Indicator | Meaning |
|---|---|
| Red ring "No API Key configured" | The API key field is empty. The fetch is not started; the error is also reported via node.error. |
| Grey ring "No locations configured" | The location list is empty. The fetch is not started. |
| Blue dot "Fetching…" | The fetch is running. |
| Green dot "Active: n" | All n locations were queried successfully. |
| Yellow dot "Active: n, Errors: m" | n locations succeeded, m locations failed. |
| Red ring "Fetch Error" | No location could be queried. |
| Error "Error fetching UV data (location): …" | The request to /uv failed for this location, for example because of an invalid key, an exhausted quota or a timeout. |
| Warning "Error fetching forecast data (location): …" | The request to /forecast failed; the observation was emitted anyway. |
Errors are reported via node.error and can be handled with a Catch node.
Notes
- Quota and rate limits: OpenUV.io limits the number of requests per day depending on the plan. Every trigger causes one request per location, two with forecast enabled. With 5 locations and hourly polling this amounts to 120 or 240 requests per day. At night OpenUV.io returns a UV index of 0; save requests by triggering only between sunrise and sunset.
- Terms of use: Use of the API is subject to the OpenUV.io terms. Check whether your plan permits publication of the data.
- Context storage: The node stores no data in the Node-RED context.
- Proxy: There is no separate proxy setting;
axioshonours the environment variablesHTTP_PROXYandHTTPS_PROXY. - Units: UV index dimensionless on the WHO scale, ozone in Dobson units, safe exposure times in minutes, sun position in radians.
- External services in the editor: The map loads tiles from OpenStreetMap; the browser running the Node-RED editor needs internet access for this.
- Personal data: The output contains coordinates and the location names you assigned. Do not use names of private individuals or private addresses as location names if the data is published.
- Data model: The output types
UVIndexObservationandUVIndexForecastfollow the conventions of the Smart Data Models (flat attributes, GeoProperty forlocation).