"All my life, I’ve thought:
if God had meant us to fly, he would have made it easier to get to the airport."
Funding decision is to be delivered Sept 9th
Apart from us benefitting from local flights, the airport will then start submitting METAR, SPECI, & TAF weather data again, which will provide great local weather reports. (bringing the topic back to weather again!)
For people on the Linconshire/South Yorkshire lowlands - but not, I'm afraid, for Sheffield!
I’m still using OpenWeatherMap, with a simple HTTP request node. It gives everything I need. I have found the rain forecast to be accurate on a par with Apple (previously DarkSky) which was amazing for minutely rain. (I'm basing that on UK weather, specifically W London)
- 60 minute rain forecast
- 0-48 hours
- 0-7 days
Below I’m using alongside Met Office for alerts, but the rain data comes from OpenWeatherMap.
(Ignore the silly “rain stopping” value, I injected dummy data from last year as there’s no rain over the next week here to show!)
That looks like a handy flow, do you mind sharing it please ?
Can you post your flow for OpenWeatherMap please? Is the http request using the REST API or do you parse http code from webpage intended for human users?
@TotallyInformation I am located in Stuttgart and interested in daily sunshine forecast. As we certainly need to reduce our CO2 emissions, this value is usefull to govern battery discharge in the night and morning hours up to about 9am. As I am not that close to the equator as Texas or Florida, the estimated radiation energy between 9 am and sunset is ideal data to collect experience to calculate required battery capacity for grid defection.
Here are my updated master weather API calls.
I currently have 4 different sources. The 2nd line on each is the start of an attempt to create a standard output set for different sources. Not sure I'll ever complete that though.
I'm missing the UK Met Office currently, I need to redo that one completely.
The common function node at the top does a rough straight-line distance calculation so that I can see how far away from my real location the forecast is for. In some cases, this can be significant and if, like me, you are somewhere where the weather can change dramatically over a short distance, this may be important.
All the outputs go into a retained global called weatherForecasts
. All of the source location and API keys are in a retained global called weather_apis
.
[{"id":"4ab807ec65746b11","type":"group","z":"d915a360.b8766","name":"https://open-meteo.com/","style":{"label":true},"nodes":["be82e6132c0c08ba","22e19dbf0931f8dd","a6fbd6322a2dc34d","985bee5f132f2846","bbcf97859e4898ce","973f033802cdcf34","64815a49346ce397","da6960f41c577986","ddd081ffc317457f","c37ff5fb216f3ddf","8577fe53a891b83c","c93dcaa0928d7285"],"x":24,"y":459,"w":1002,"h":162},{"id":"be82e6132c0c08ba","type":"change","z":"d915a360.b8766","g":"4ab807ec65746b11","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"(\t /* Exclude = current,minutely,hourly,daily */\t \t $apis := $globalContext(\"weather_apis\", \"file\");\t \t {\t \"latitude\": $apis.lat,\t \"longitude\": $apis.lon,\t \"current_weather\": true,\t \"altitude\": $apis.alt,\t \"hourly\": \"temperature_2m,relativehumidity_2m,windspeed_10m\"\t }\t)","tot":"jsonata"},{"t":"set","p":"url","pt":"msg","to":"https://api.open-meteo.com/v1/forecast","tot":"str"},{"t":"set","p":"headers","pt":"msg","to":"{\"Accept\":\"application/json\",\"User-Agent\":\"node-red\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":145,"y":500,"wires":[["a6fbd6322a2dc34d"]],"l":false},{"id":"22e19dbf0931f8dd","type":"inject","z":"d915a360.b8766","g":"4ab807ec65746b11","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":85,"y":500,"wires":[["be82e6132c0c08ba"]],"l":false},{"id":"a6fbd6322a2dc34d","type":"http request","z":"d915a360.b8766","g":"4ab807ec65746b11","name":"","method":"GET","ret":"obj","paytoqs":"query","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":195,"y":500,"wires":[["973f033802cdcf34"]],"l":false},{"id":"985bee5f132f2846","type":"debug","z":"d915a360.b8766","g":"4ab807ec65746b11","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":665,"y":500,"wires":[],"l":false},{"id":"bbcf97859e4898ce","type":"comment","z":"d915a360.b8766","g":"4ab807ec65746b11","name":"Up to 16 days forecast Fair use policy. Maximum 10,000 calls/day No API key required","info":"","x":350,"y":540,"wires":[]},{"id":"973f033802cdcf34","type":"change","z":"d915a360.b8766","g":"4ab807ec65746b11","name":"set global.weatherForecasts.openMeteo","rules":[{"t":"set","p":"#:(file)::weatherForecasts.openMeteo","pt":"global","to":"payload","tot":"msg"},{"t":"set","p":"#:(file)::weatherForecasts.openMeteo.lastUpdate","pt":"global","to":"payload.current_weather.time","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":500,"wires":[["985bee5f132f2846"]]},{"id":"64815a49346ce397","type":"inject","z":"d915a360.b8766","g":"4ab807ec65746b11","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":85,"y":580,"wires":[["da6960f41c577986"]],"l":false},{"id":"da6960f41c577986","type":"function","z":"d915a360.b8766","g":"4ab807ec65746b11","name":"","func":"const $full = global.get('weatherForecasts.openMeteo', 'file')\n\nmsg.payload = {\n \"source\": \"Open Meteo API\",\n \"locn\": {\n \"lat\": $full.latitude,\n \"lon\": $full.longitude,\n \"alt\": $full.elevation,\n \"name\": \"Broomhill\",\n },\n \"updated\": $full.current_weather.time,\n \"full\": $full,\n \"distance\": {\n \"from\": {\n \"lat\": global.get('weather_apis.lat', 'file'),\n \"lon\": global.get('weather_apis.lon', 'file'),\n },\n \"to\": {\n \"lat\": $full.latitude,\n \"lon\": $full.longitude,\n },\n \"unit\": \"m\",\n },\n}\n\nconst forecasts = $full.hourly\n\nconst myNow = new Date() //Date.now()\nconst out = {} //[]\n\nfor (let i = 0; i < forecasts.time.length; i++) {\n const time = forecasts.time[i]\n let ts = new Date(time)\n if (ts >= myNow) {\n // node.warn(`${i}: ${time}...`)\n let splitDt = time.split('T')\n let day = splitDt[0]\n let hr = splitDt[1].slice(0, 2)\n\n if (!out[day]) out[day] = {}\n\n const o = {\n temperature_2m: forecasts.temperature_2m[i],\n relativehumidity_2m: forecasts.relativehumidity_2m[i],\n windspeed_10m: forecasts.windspeed_10m[i],\n }\n node.warn(JSON.stringify(o))\n\n out[day][hr] = o\n }\n}\n\nmsg.payload.hourly = out\n\nreturn msg","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":145,"y":580,"wires":[["c37ff5fb216f3ddf"]],"l":false},{"id":"ddd081ffc317457f","type":"debug","z":"d915a360.b8766","g":"4ab807ec65746b11","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":665,"y":580,"wires":[],"l":false},{"id":"c37ff5fb216f3ddf","type":"link call","z":"d915a360.b8766","g":"4ab807ec65746b11","name":"","links":["8de8d5547f88456d"],"linkType":"static","timeout":"1","x":260,"y":580,"wires":[["8577fe53a891b83c"]]},{"id":"8577fe53a891b83c","type":"change","z":"d915a360.b8766","g":"4ab807ec65746b11","name":"","rules":[{"t":"set","p":"payload.locn.distance","pt":"msg","to":"payload.distance.howFar","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":490,"y":580,"wires":[["ddd081ffc317457f"]]},{"id":"c93dcaa0928d7285","type":"comment","z":"d915a360.b8766","g":"4ab807ec65746b11","name":"Open Meteo","info":"","x":930,"y":500,"wires":[]},{"id":"815941740bc0e964","type":"group","z":"d915a360.b8766","name":"Open Weather Map OneCall API. API Key and lat/lon in global.weather_apis","style":{"label":true,"fill":"#e3f3d3","fill-opacity":"0.29","color":"#000000"},"nodes":["3817c8b2947d01f6","0ac196566feba620","166c313a.49d2ef","a58a05c.aade6f8","e844d1e8.f8585","dbd12e73.be8cb","3bbcfa11.167be6","53c45f68.00321","8e6b9e728b78b1f0","83a5a47c39e68b98","421a1883e4eba346","287017c287456989","72698cef80389b7f","31064ce2e82257f4","71d6dd69d45b7120","6773d1d5260511d7","5a23863cf25592a5","a5b636ef3809c1cc","42cd887b60101b56","aab6e98d2f6c37be","ec2de20d24c3bdbc","47fec5cdcd523dd8"],"x":24,"y":639,"w":1002,"h":282},{"id":"3817c8b2947d01f6","type":"change","z":"d915a360.b8766","g":"815941740bc0e964","name":"","rules":[{"t":"set","p":"url","pt":"msg","to":"(\t $apis := $globalContext(\"weather_apis\", \"file\");\t \"http://api.openweathermap.org/data/2.5/air_pollution?lat=\" & $apis.lat & \"&lon=\" & $apis.lon & \"&appid=\" & $apis.keys.owm\t)\t","tot":"jsonata"},{"t":"set","p":"headers","pt":"msg","to":"{\"content-type\":\"application/json\",\"Accept\":\"application/json\",\"User-Agent\":\"node-red\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":155,"y":880,"wires":[["287017c287456989"]],"l":false},{"id":"0ac196566feba620","type":"inject","z":"d915a360.b8766","g":"815941740bc0e964","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":95,"y":880,"wires":[["3817c8b2947d01f6"]],"l":false},{"id":"166c313a.49d2ef","type":"change","z":"d915a360.b8766","g":"815941740bc0e964","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"(\t /* Exclude = current,minutely,hourly,daily */\t \t $apis := $globalContext(\"weather_apis\", \"file\");\t \t {\t \"appid\": $apis.keys.owm,\t \"lat\": $apis.lat,\t \"lon\": $apis.lon\t /* \"exclude\": \"current,minutely,daily\" */\t }\t)","tot":"jsonata"},{"t":"set","p":"url","pt":"msg","to":"https://api.openweathermap.org/data/3.0/onecall","tot":"str"},{"t":"set","p":"headers","pt":"msg","to":"{\"content-type\":\"application/json\",\"Accept\":\"application/json\",\"User-Agent\":\"node-red\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":145,"y":680,"wires":[["e844d1e8.f8585"]],"l":false},{"id":"a58a05c.aade6f8","type":"inject","z":"d915a360.b8766","g":"815941740bc0e964","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":85,"y":680,"wires":[["166c313a.49d2ef"]],"l":false},{"id":"e844d1e8.f8585","type":"http request","z":"d915a360.b8766","g":"815941740bc0e964","name":"","method":"GET","ret":"obj","paytoqs":true,"url":"","tls":"","persist":false,"proxy":"","authType":"","x":195,"y":680,"wires":[["dbd12e73.be8cb"]],"l":false},{"id":"dbd12e73.be8cb","type":"change","z":"d915a360.b8766","g":"815941740bc0e964","name":"set global.weatherForecasts.openWeatherMap","rules":[{"t":"set","p":"#:(file)::weatherForecasts.openWeatherMap","pt":"global","to":"payload","tot":"msg"},{"t":"set","p":"#:(file)::weatherForecasts.openWeatherMap.lastUpdate","pt":"global","to":"","tot":"date"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":680,"wires":[["3bbcfa11.167be6"]]},{"id":"3bbcfa11.167be6","type":"debug","z":"d915a360.b8766","g":"815941740bc0e964","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":665,"y":680,"wires":[],"l":false},{"id":"53c45f68.00321","type":"comment","z":"d915a360.b8766","g":"815941740bc0e964","name":"Open Weather Map [OWM] (Onecall)","info":"","x":860,"y":680,"wires":[]},{"id":"8e6b9e728b78b1f0","type":"comment","z":"d915a360.b8766","g":"815941740bc0e964","name":"OpenWeatherMap's OneCall API https://openweathermap.org/api/one-call-3 - free tier gives 1000 calls per day.","info":"https://openweathermap.org/api/one-call-3\n\nIcons:\nhttps://flows.nodered.org/flow/d7af9f5d6f6923324466c58ea4f0655f","x":430,"y":720,"wires":[]},{"id":"83a5a47c39e68b98","type":"debug","z":"d915a360.b8766","g":"815941740bc0e964","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.list","targetType":"msg","statusVal":"","statusType":"auto","x":655,"y":880,"wires":[],"l":false},{"id":"421a1883e4eba346","type":"comment","z":"d915a360.b8766","g":"815941740bc0e964","name":"OWM Air Quality API","info":"https://openweathermap.org/api/air-pollution\n\nHere is a description of Air Quality Index levels:\n\n| Qualitative name | Index | Pollutant concentration in μg/m<sup>3</sup> |\n| --- | --- | --- |\n| | SO<sub>2</sub> | NO<sub>2</sub> | PM<sub>10</sub> | PM<sub>2.5</sub> | O<sub>3</sub> | CO |\n| Good | 1 | \\[0; 20) | \\[0; 40) | \\[0; 20) | \\[0; 10) | \\[0; 60) | \\[0; 4400) |\n| Fair | 2 | \\[20; 80) | \\[40; 70) | \\[20; 50) | \\[10; 25) | \\[60; 100) | \\[4400; 9400) |\n| Moderate | 3 | \\[80; 250) | \\[70; 150) | \\[50; 100) | \\[25; 50) | \\[100; 140) | \\[9400-12400) |\n| Poor | 4 | \\[250; 350) | \\[150; 200) | \\[100; 200) | \\[50; 75) | \\[140; 180) | \\[12400; 15400) |\n| Very Poor | 5 | ⩾350 | ⩾200 | ⩾200 | ⩾75 | ⩾180 | ⩾15400 |","x":820,"y":880,"wires":[]},{"id":"287017c287456989","type":"http request","z":"d915a360.b8766","g":"815941740bc0e964","name":"Current Air Quality (Home)","method":"GET","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":320,"y":880,"wires":[["83a5a47c39e68b98"]]},{"id":"72698cef80389b7f","type":"debug","z":"d915a360.b8766","g":"815941740bc0e964","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":655,"y":840,"wires":[],"l":false},{"id":"31064ce2e82257f4","type":"comment","z":"d915a360.b8766","g":"815941740bc0e964","name":"OWM Geocoding API","info":"https://openweathermap.org/api/geocoding-api","x":830,"y":840,"wires":[]},{"id":"71d6dd69d45b7120","type":"inject","z":"d915a360.b8766","g":"815941740bc0e964","name":"","props":[{"p":"lat","v":"#:(file)::weather_apis.lat","vt":"global"},{"p":"lon","v":"#:(file)::weather_apis.lon","vt":"global"},{"p":"lat","v":"53.38","vt":"str"},{"p":"lon","v":"-1.5","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":95,"y":840,"wires":[["6773d1d5260511d7"]],"l":false},{"id":"6773d1d5260511d7","type":"change","z":"d915a360.b8766","g":"815941740bc0e964","name":"","rules":[{"t":"set","p":"url","pt":"msg","to":"(\t $apis := $globalContext(\"weather_apis\", \"file\");\t \"http://api.openweathermap.org/geo/1.0/reverse?lat=\" & lat & \"&lon=\" & lon & \"&limit=10&appid=\" & $apis.keys.owm\t)\t","tot":"jsonata"},{"t":"set","p":"headers","pt":"msg","to":"{\"content-type\":\"application/json\",\"Accept\":\"application/json\",\"User-Agent\":\"node-red\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":155,"y":840,"wires":[["5a23863cf25592a5"]],"l":false,"info":"https://developer.climacell.co/v3/reference#get-realtime\n\nhttps://developer.climacell.co/v3/docs/present\n\nhttps://developer.climacell.co/v3/widgets"},{"id":"5a23863cf25592a5","type":"http request","z":"d915a360.b8766","g":"815941740bc0e964","name":"Reverse Geocode (Home)","method":"GET","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":310,"y":840,"wires":[["72698cef80389b7f"]]},{"id":"a5b636ef3809c1cc","type":"inject","z":"d915a360.b8766","g":"815941740bc0e964","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":85,"y":760,"wires":[["42cd887b60101b56"]],"l":false},{"id":"42cd887b60101b56","type":"function","z":"d915a360.b8766","g":"815941740bc0e964","name":"","func":"const $full = global.get('weatherForecasts.openMeteo', 'file')\n\nmsg.payload = {\n \"source\": \"Open Meteo API\",\n \"locn\": {\n \"lat\": $full.latitude,\n \"lon\": $full.longitude,\n \"alt\": $full.elevation,\n \"name\": \"Broomhill\",\n },\n \"updated\": $full.current_weather.time,\n \"full\": $full,\n \"distance\": {\n \"from\": {\n \"lat\": global.get('weather_apis.lat', 'file'),\n \"lon\": global.get('weather_apis.lon', 'file'),\n },\n \"to\": {\n \"lat\": $full.latitude,\n \"lon\": $full.longitude,\n },\n \"unit\": \"m\",\n },\n}\n\nconst forecasts = $full.hourly\n\nconst myNow = new Date() //Date.now()\nconst out = {} //[]\n\nfor (let i = 0; i < forecasts.time.length; i++) {\n const time = forecasts.time[i]\n let ts = new Date(time)\n if (ts >= myNow) {\n // node.warn(`${i}: ${time}...`)\n let splitDt = time.split('T')\n let day = splitDt[0]\n let hr = splitDt[1].slice(0, 2)\n\n if (!out[day]) out[day] = {}\n\n const o = {\n temperature_2m: forecasts.temperature_2m[i],\n relativehumidity_2m: forecasts.relativehumidity_2m[i],\n windspeed_10m: forecasts.windspeed_10m[i],\n }\n node.warn(JSON.stringify(o))\n\n out[day][hr] = o\n }\n}\n\nmsg.payload.hourly = out\n\nreturn msg","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":145,"y":760,"wires":[["ec2de20d24c3bdbc"]],"l":false},{"id":"aab6e98d2f6c37be","type":"debug","z":"d915a360.b8766","g":"815941740bc0e964","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":665,"y":760,"wires":[],"l":false},{"id":"ec2de20d24c3bdbc","type":"link call","z":"d915a360.b8766","g":"815941740bc0e964","name":"","links":["8de8d5547f88456d"],"linkType":"static","timeout":"1","x":260,"y":760,"wires":[["47fec5cdcd523dd8"]]},{"id":"47fec5cdcd523dd8","type":"change","z":"d915a360.b8766","g":"815941740bc0e964","name":"","rules":[{"t":"set","p":"payload.locn.distance","pt":"msg","to":"payload.distance.howFar","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":490,"y":760,"wires":[["aab6e98d2f6c37be"]]},{"id":"9f00354b1491eeb3","type":"group","z":"d915a360.b8766","name":"Storm Glass","style":{"label":true},"nodes":["73306a5b.3dfd54","52baafeb.8eab6","abc3fd14.e68ce","a12a90b1.06532","32229272.65d27e","b990f32.16efe1","825825.0d06a7d8","1514d2cc.85660d","d43a5845.c079e8","7dc6301173a5ab60","3bd0fc58cf80745e"],"x":24,"y":159,"w":1002,"h":122},{"id":"73306a5b.3dfd54","type":"change","z":"d915a360.b8766","g":"9f00354b1491eeb3","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"(\t /* https://docs.stormglass.io/#/weather */\t \t $apis := $globalContext(\"weather_apis\", \"file\");\t\t $params := [\t \"airTemperature\",\t \"airTemperature80m\",\t \"airTemperature100m\",\t \"airTemperature1000hpa\",\t \"airTemperature800hpa\",\t \"airTemperature500hpa\",\t \"airTemperature200hpa\",\t \"pressure\",\t \"cloudCover\",\t \"currentDirection\",\t \"currentSpeed\",\t \"gust\",\t \"humidity\",\t \"iceCover\",\t \"precipitation\",\t \"snowDepth\",\t \"seaLevel\",\t \"swellDirection\",\t \"swellHeight\",\t \"swellPeriod\",\t \"secondarySwellPeriod\",\t \"secondarySwellDirection\",\t \"secondarySwellHeight\",\t /*\"visiblity\",*/\t \"waterTemperature\",\t \"waveDirection\",\t \"waveHeight\",\t \"wavePeriod\",\t \"windWaveDirection\",\t \"windWaveHeight\",\t \"windWavePeriod\",\t \"windDirection\",\t \"windDirection20m\",\t \"windDirection30m\",\t \"windDirection40m\",\t \"windDirection50m\",\t \"windDirection80m\",\t \"windDirection100m\",\t \"windDirection1000hpa\",\t \"windDirection800hpa\",\t \"windDirection500hpa\",\t \"windDirection200hpa\",\t \"windSpeed\",\t \"windSpeed20m\",\t \"windSpeed30m\",\t \"windSpeed40m\",\t \"windSpeed50m\",\t \"windSpeed80m\",\t \"windSpeed100m\",\t \"windSpeed1000hpa\",\t \"windSpeed800hpa\",\t \"windSpeed500hpa\",\t \"windSpeed200hpa\"\t ];\t \t {\t \"lat\": $apis.lat,\t \"lng\": $apis.lon,\t \"params\": $join($params, \",\"),\t \"start\": $now()\t }\t)","tot":"jsonata"},{"t":"set","p":"url","pt":"msg","to":"https://api.stormglass.io/v2/weather/point","tot":"str"},{"t":"set","p":"headers","pt":"msg","to":"(\t $apis := $globalContext(\"weather_apis\", \"file\");\t\t {\t \"Authorization\": $apis.keys.stormglass,\t \"content-type\":\"application/json\",\t \"Accept\":\"application/json\",\t \"User-Agent\":\"node-red\"\t }\t)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":145,"y":200,"wires":[["abc3fd14.e68ce"]],"l":false},{"id":"52baafeb.8eab6","type":"inject","z":"d915a360.b8766","g":"9f00354b1491eeb3","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":85,"y":200,"wires":[["73306a5b.3dfd54"]],"l":false},{"id":"abc3fd14.e68ce","type":"http request","z":"d915a360.b8766","g":"9f00354b1491eeb3","name":"","method":"GET","ret":"obj","paytoqs":true,"url":"","tls":"","persist":false,"proxy":"","authType":"","x":205,"y":200,"wires":[["a12a90b1.06532"]],"l":false},{"id":"a12a90b1.06532","type":"change","z":"d915a360.b8766","g":"9f00354b1491eeb3","name":"set global.weatherForecasts.stormGlass","rules":[{"t":"set","p":"#:(file)::weatherForecasts.stormGlass","pt":"global","to":"payload","tot":"msg"},{"t":"set","p":"weatherForecasts.stormGlass.lastUpdate","pt":"msg","to":"payload.meta.start","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":200,"wires":[["32229272.65d27e"]]},{"id":"32229272.65d27e","type":"debug","z":"d915a360.b8766","g":"9f00354b1491eeb3","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":665,"y":200,"wires":[],"l":false},{"id":"b990f32.16efe1","type":"inject","z":"d915a360.b8766","g":"9f00354b1491eeb3","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":85,"y":240,"wires":[["1514d2cc.85660d"]],"l":false},{"id":"825825.0d06a7d8","type":"debug","z":"d915a360.b8766","g":"9f00354b1491eeb3","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":665,"y":240,"wires":[],"l":false},{"id":"1514d2cc.85660d","type":"function","z":"d915a360.b8766","g":"9f00354b1491eeb3","name":"","func":"/* https://docs.stormglass.io/#/weather */\n\nconst $full = global.get('weatherForecasts.stormGlass', 'file')\n\nmsg.payload = {\n \"source\": \"Stormglass Weather API\",\n \"locn\": {\n \"lat\": $full.meta.lat,\n \"lon\": $full.meta.lng,\n \"alt\": global.get('weather_apis.alt', 'file'),\n \"name\": \"Home\",\n },\n \"updated\": $full.hours[0].time,\n \"full\": $full,\n \"distance\": {\n \"from\": {\n \"lat\": global.get('weather_apis.lat', 'file'),\n \"lon\": global.get('weather_apis.lon', 'file'),\n },\n \"to\": {\n \"lat\": $full.meta.lat,\n \"lon\": $full.meta.lng,\n },\n \"unit\": \"m\",\n \"howFar\": 0,\n },\n}\n\nconst forecasts = $full.hours\n\nconst myNow = new Date() //Date.now()\nconst out = {} //[]\n\nforecasts.forEach( obj => {\n const time = obj.time\n let ts = new Date(time)\n if ( ts < myNow) return\n \n let splitDt = time.split('T')\n let day = splitDt[0]\n let hr = splitDt[1].slice(0,2)\n \n if ( ! out[day] ) out[day] = {}\n \n const o = out[day][hr] = {}\n \n const inp = obj // in case we need to drill deeper to get the data\n \n Object.keys(inp).forEach( key => {\n // Which bits to throw away\n if ( key === 'time' ) return\n \n const newKey = key // Allow for renaming keys\n \n if ( inp[key].sg ) o[newKey] = inp[key].sg\n else if ( inp[key].dwd ) o[newKey] = inp[key].dwd\n else if ( inp[key].noaa ) o[newKey] = inp[key].noaa\n })\n \n})\n\nmsg.payload.hours = out\n\nreturn msg","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":145,"y":240,"wires":[["825825.0d06a7d8"]],"l":false},{"id":"d43a5845.c079e8","type":"comment","z":"d915a360.b8766","g":"9f00354b1491eeb3","name":"Storm Glass (10 reqs/d)","info":"","x":900,"y":200,"wires":[]},{"id":"7dc6301173a5ab60","type":"link call","z":"d915a360.b8766","g":"9f00354b1491eeb3","name":"","links":["8de8d5547f88456d"],"linkType":"static","timeout":"1","x":260,"y":240,"wires":[["3bd0fc58cf80745e"]]},{"id":"3bd0fc58cf80745e","type":"change","z":"d915a360.b8766","g":"9f00354b1491eeb3","name":"set msg.payload.locn.distance","rules":[{"t":"set","p":"payload.locn.distance","pt":"msg","to":"0","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":490,"y":240,"wires":[[]]},{"id":"d2034c44d488b76c","type":"group","z":"d915a360.b8766","name":"Norway Met Office","style":{"label":true},"nodes":["3f8f794d.c746d6","efbfede3.dd015","25f169ec.5d3956","ef42304a.2a60a","f82d1f8e.bc4fd","ee8a2648.6e4eb8","99eb6f4.1640c9","39e82d5e.a146c2","1cf90e57.32d332","4786801ceb348a72","86a872feaccb4b7d"],"x":24,"y":319,"w":1002,"h":122},{"id":"3f8f794d.c746d6","type":"change","z":"d915a360.b8766","g":"d2034c44d488b76c","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"(\t $apis := $globalContext('weather_apis');\t\t {\t \"lat\": $apis.lat,\t \"lon\": $apis.lon,\t \"altitude\": $apis.alt\t }\t)","tot":"jsonata"},{"t":"set","p":"url","pt":"msg","to":"https://api.met.no/weatherapi/locationforecast/2.0/complete.json","tot":"str"},{"t":"set","p":"headers","pt":"msg","to":"{\"Accept\":\"application/json\",\"User-Agent\":\"node-red\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":145,"y":360,"wires":[["25f169ec.5d3956"]],"l":false,"info":"https://api.met.no/weatherapi/documentation"},{"id":"efbfede3.dd015","type":"inject","z":"d915a360.b8766","g":"d2034c44d488b76c","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":85,"y":360,"wires":[["3f8f794d.c746d6"]],"l":false},{"id":"25f169ec.5d3956","type":"http request","z":"d915a360.b8766","g":"d2034c44d488b76c","name":"","method":"GET","ret":"obj","paytoqs":"query","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":195,"y":360,"wires":[["ef42304a.2a60a"]],"l":false},{"id":"ef42304a.2a60a","type":"change","z":"d915a360.b8766","g":"d2034c44d488b76c","name":"set weatherForecasts.norwayMet","rules":[{"t":"set","p":"#:(file)::weatherForecasts.norwayMet","pt":"global","to":"payload","tot":"msg"},{"t":"set","p":"#:(file)::weatherForecasts.norwayMet.lastUpdate","pt":"global","to":"payload.properties.meta.updated_at","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":380,"y":360,"wires":[["f82d1f8e.bc4fd"]]},{"id":"f82d1f8e.bc4fd","type":"debug","z":"d915a360.b8766","g":"d2034c44d488b76c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":665,"y":360,"wires":[],"l":false},{"id":"ee8a2648.6e4eb8","type":"inject","z":"d915a360.b8766","g":"d2034c44d488b76c","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":85,"y":400,"wires":[["39e82d5e.a146c2"]],"l":false},{"id":"99eb6f4.1640c9","type":"debug","z":"d915a360.b8766","g":"d2034c44d488b76c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":665,"y":400,"wires":[],"l":false},{"id":"39e82d5e.a146c2","type":"function","z":"d915a360.b8766","g":"d2034c44d488b76c","name":"","func":"const $full = global.get('weatherForecasts.norwayMet', 'file')\n\nmsg.payload = {\n \"source\": \"Norway MET Weather API\",\n \"locn\": {\n \"lat\": $full.geometry.coordinates[1],\n \"lon\": $full.geometry.coordinates[0],\n \"alt\": $full.geometry.coordinates[2],\n \"name\": \"Home\"\n },\n \"updated\": $full.properties.meta.updated_at,\n \"full\": $full,\n \"distance\": {\n \"from\": {\n \"lat\": global.get('weather_apis.lat', 'file'),\n \"lon\": global.get('weather_apis.lon', 'file'),\n },\n \"to\": {\n \"lat\": $full.geometry.coordinates[1],\n \"lon\": $full.geometry.coordinates[0]\n },\n \"unit\": \"m\"\n }\n}\n\nconst forecasts = $full.properties.timeseries\n\nconst myNow = new Date() //Date.now()\nconst out = {} //[]\n\nforecasts.forEach( obj => {\n const time = obj.time\n let ts = new Date(time)\n if ( ts < myNow) return\n \n let splitDt = time.split('T')\n let day = splitDt[0]\n let hr = splitDt[1].slice(0,2)\n \n if ( ! out[day] ) out[day] = {}\n \n const o = out[day][hr] = {}\n \n const inp = obj.data.instant.details\n \n Object.keys(inp).forEach( key => {\n // Which bits to throw away\n if ( key === 'time' ) return\n \n const newKey = key // Allow for renaming keys\n \n o[newKey] = inp[key]\n })\n \n if ( obj.data.next_1_hours ) {\n o.next_hour = {}\n o.next_hour.summary = obj.data.next_1_hours.summary.symbol_code\n \n o.next_hour.precipitation_amount = obj.data.next_1_hours.details.precipitation_amount\n }\n})\n\nmsg.payload.hourly = out\n\nreturn msg","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":145,"y":400,"wires":[["4786801ceb348a72"]],"l":false},{"id":"1cf90e57.32d332","type":"comment","z":"d915a360.b8766","g":"d2034c44d488b76c","name":"Norway MET","info":"","x":930,"y":360,"wires":[]},{"id":"4786801ceb348a72","type":"link call","z":"d915a360.b8766","g":"d2034c44d488b76c","name":"","links":["8de8d5547f88456d"],"linkType":"static","timeout":"1","x":260,"y":400,"wires":[["86a872feaccb4b7d"]]},{"id":"86a872feaccb4b7d","type":"change","z":"d915a360.b8766","g":"d2034c44d488b76c","name":"","rules":[{"t":"set","p":"payload.locn.distance","pt":"msg","to":"payload.distance.howFar","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":490,"y":400,"wires":[["99eb6f4.1640c9"]]},{"id":"f58e0f157b6ecc7e","type":"group","z":"d915a360.b8766","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["e2a9f98faf258b5c","8de8d5547f88456d","add2b9c307d4eddd"],"x":24,"y":19,"w":442,"h":82},{"id":"e2a9f98faf258b5c","type":"function","z":"d915a360.b8766","g":"f58e0f157b6ecc7e","name":"Output distance between 2 points","func":"function calculateDistance(lat1, lon1, lat2, lon2, unit = 'km') {\n lat1 = parseFloat(lat1)\n lon1 = parseFloat(lon1)\n lat2 = parseFloat(lat2)\n lon2 = parseFloat(lon2)\n // Validate input parameters\n if (typeof lat1 !== 'number' || typeof lon1 !== 'number'\n || typeof lat2 !== 'number' || typeof lon2 !== 'number') {\n throw new Error('All coordinates must be numbers')\n }\n\n if (lat1 < -90 || lat1 > 90 || lat2 < -90 || lat2 > 90) {\n throw new Error('Latitude must be between -90 and 90 degrees')\n }\n\n if (lon1 < -180 || lon1 > 180 || lon2 < -180 || lon2 > 180) {\n throw new Error('Longitude must be between -180 and 180 degrees')\n }\n\n // Earth's radius in kilometers\n const earthRadiusKm = 6371\n\n // Convert degrees to radians\n const lat1Rad = lat1 * Math.PI / 180\n const lon1Rad = lon1 * Math.PI / 180\n const lat2Rad = lat2 * Math.PI / 180\n const lon2Rad = lon2 * Math.PI / 180\n\n // Calculate differences\n const deltaLat = lat2Rad - lat1Rad\n const deltaLon = lon2Rad - lon1Rad\n\n // Haversine formula\n const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2)\n + Math.cos(lat1Rad) * Math.cos(lat2Rad)\n * Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2)\n\n const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))\n\n // Distance in kilometers\n const distanceKm = earthRadiusKm * c\n\n let outDist\n\n // Convert to requested unit\n switch (unit.toLowerCase()) {\n case 'km':\n outDist = distanceKm\n break\n case 'mi':\n outDist = distanceKm * 0.621371\n break\n case 'm':\n outDist = distanceKm * 1000\n break\n default:\n throw new Error('Unit must be \"km\", \"mi\", or \"m\"')\n }\n return Math.round(outDist) ?? 0\n}\n\nmsg.payload.distance.howFar = calculateDistance(\n msg.payload.distance.from.lat, msg.payload.distance.from.lon,\n msg.payload.distance.to.lat, msg.payload.distance.to.lon,\n msg.payload.distance.unit ?? 'km'\n)\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":250,"y":60,"wires":[["add2b9c307d4eddd"]]},{"id":"8de8d5547f88456d","type":"link in","z":"d915a360.b8766","g":"f58e0f157b6ecc7e","name":"distance calc in","links":[],"x":65,"y":60,"wires":[["e2a9f98faf258b5c"]]},{"id":"add2b9c307d4eddd","type":"link out","z":"d915a360.b8766","g":"f58e0f157b6ecc7e","name":"distance out","mode":"return","links":[],"x":425,"y":60,"wires":[]}]
Hi, so I use subflows and it was a bit of a faff copying the flows and removing personal data, so hopefully my step-by-step explanation below will allow you to recreate it.
Inject node - e.g. 5 minute repeat interval
Function node (name it "setup" or something)
var appid = "insert your app id here"
msg.location = msg.payload;
msg.lat = 51.510357;
msg.long = -0.116773;
msg.url="https://api.openweathermap.org/data/3.0/onecall?lat=" + msg.lat + "&lon=" + msg.long + "&units=metric&appid=" + appid;
return msg;
HTTP Request Node
Set method to GET
Wire the three nodes in that order, and the output of the HTTP Request node will give you everything, so you can then process it later. Let me know if you'd like to see more function nodes that process into graphs etc
That's why mine use a standard global context variable for all the sensitive bits. The keys, and my actual location. That way I can always copy the flows and freely share them.
I think that's the interesting bit
I came across open-meteo which has quite an extensive api from consolidated sources and does not require authentication or signup. Anyone ever tried this ?
The OWM data feed has a lot of data… I really like how you turn it into graphs which are very easy to “see” the data. I would really like to see more details/functions on how you feed the data to those charts.
FYI… I use this node to feed my credentials to the OWM http node but it “hides” my credentials so I can upload/post my flows without including my personal data.\
Yes, it is in the example I shared above.
Up to 16 days forecast Fair use policy. Maximum 10,000 calls/day No API key required.
That is my interpretation of the data. There are also per-minute and per-day outputs.
The data isn't in the most convenient format and there is a LOT of data if you don't filter it.
But more importantly, how accurate is the data?
Same question for all above services.
It's a nice exercise creating a weather dashboard with charts, gauges etc. however it's hard to compete with apps such as Met Office, BBC weather, The Weather Channel to mention a few.
Honestly, I've never actually done a comparison. However, they combine data from a LOT of different sources including Met Office, Norway Met, ... and they produce highly localised forecasts. So should be good. But, as I say, I've not tested in detail - it's on my to-do list!
It seems quite localized you can specify a boundary box of geo coordinates which is nice and many parameters available too.
Here's my 48 hour rain function. I just feed this function into a Dashboard template chart node:
// minutely[x].precipitation is measured in mm / h
var m = {};
m.data = [[]]
m.data[0] = []
m.labels = []
/**
* Converts a timestamp to a human-readable format, showing just the time if the date is today,
* or the abbreviated day and time if it's tomorrow or later.
*
* - For today's date, it returns the time (e.g., "3 PM").
* - For any future date, including tomorrow, it returns the abbreviated day of the week followed by the time (e.g., "Wed, 3 PM").
* - Special cases: "Midnight" for 00:00 and "Midday" for 12:00.
*
* @param {number} dt - Unix timestamp (in seconds) to be converted.
* @return {string} - Formatted date string for chart display.
*/
function readableDate(dt) {
const date = new Date(dt * 1000);
const now = new Date();
const isToday = date.getDate() === now.getDate() &&
date.getMonth() === now.getMonth() &&
date.getFullYear() === now.getFullYear();
const hours = date.getHours();
let timeString;
if (hours === 0) {
timeString = "Midnight"; // Special case for midnight
} else if (hours === 12) {
timeString = "Midday"; // Special case for midday
} else {
// Format other times as "3 AM", "4 PM", etc.
timeString = date.toLocaleString('en-GB', {
hour: 'numeric',
minute: 'numeric',
hour12: true,
timeZone: 'Europe/London'
}).replace(':00', ''); // Remove minutes if zero
}
if (isToday) {
return timeString; // Only show time for today
} else {
return `${date.toLocaleDateString('en-GB', { weekday: 'short' })}, ${timeString}`; // "Wed, 3 PM"
}
}
for (var i = 0; i < 48; i++) {
// Ensure hourly data exists and is an array with enough elements
if (msg.payload.hourly && Array.isArray(msg.payload.hourly) && msg.payload.hourly.length > i) {
// label every x hours
m.labels.push(i % 12 === 0 ? readableDate(msg.payload.hourly[i].dt) : "");
m.data[0].push(msg.payload.hourly[i].rain ? msg.payload.hourly[i].rain['1h'] : 0);
} else {
// Handle case where data is not available
m.labels.push(""); // Empty label if no data
m.data[0].push(0); // Default rain value if no data
}
}
m.series = ['Series A'];
return { payload: [m], topic: msg.topic };
Or "next hour" chart:
// minutely[x].precipitation is measured in mm / h
var m = {};
m.data = [[]]
m.data[0] = msg.payload.minutely.map(a => a.precipitation);
m.labels = []
for (var i = 0; i < m.data[0].length; i++) {
m.labels.push(i % 10 === 0 ? i : "")
}
m.series = ['Series A'];
return { payload: [m], topic: msg.topic };
I just wish it were easier to define which X-axis ticks / labels are shown! I have not found a good way to do that.
Right now I have this:
For example, the 24 hour display only has two x-axis labels (6pm and Sat, 6am). I'd love to be able to add more.
@hazymat … I VERY much appreciate you posting your functions. This will be tremendously value for me in developing my functions. One quick question about your chart widgets. Are you setting both x and y axis to use key or msg.data ?
Just checked. The nearest "cell" to me on Open Meteo is 1km away and in a position that can have quite different weather. OWM returns something within a few metres - though how realistic that is, I don't know.
BTW, you might note that I posted an example earlier of an animated SVG wind display - speed and direction on a gauge. Based on work done for Dashboard 1 but I have converted it to work with pure JavaScript using UIBUILDER for the comms.
@hazymat … I’ve successfully created my versions of the rainfall charts but having one problem. For the Next 8 Days % chart to display the day of the week (i.e., Mon, Tue, Wed) on the x-axis for the line chart. Can you explain how you were able to get your chart to display the Day?