From 256f63c870efb0a5e9c964c4ce462226faa6b325 Mon Sep 17 00:00:00 2001 From: todd Date: Wed, 6 Oct 2021 13:33:59 -0700 Subject: [PATCH] Fixing issues with the OWM weather service after their recent changes to the API --- routes/weatherProviders/OWM.ts | 181 ++++++++++++++++++++++++++++----- 1 file changed, 153 insertions(+), 28 deletions(-) diff --git a/routes/weatherProviders/OWM.ts b/routes/weatherProviders/OWM.ts index 9aa169c..8f11301 100644 --- a/routes/weatherProviders/OWM.ts +++ b/routes/weatherProviders/OWM.ts @@ -17,13 +17,24 @@ export default class OWMWeatherProvider extends WeatherProvider { } } - public async getWateringData( coordinates: GeoCoordinates ): Promise< ZimmermanWateringData > { - const forecastUrl = `http://api.openweathermap.org/data/2.5/forecast?appid=${ this.API_KEY }&units=imperial&lat=${ coordinates[ 0 ] }&lon=${ coordinates[ 1 ] }`; + public async getWateringData(coordinates: GeoCoordinates): Promise { + // The OWM free API options changed so need to use the new API method + const forecastUrl = `https://api.openweathermap.org/data/2.5/onecall?exclude=current,minutely,daily,alerts&appid=${ this.API_KEY }&units=imperial&lat=${ coordinates[ 0 ] }&lon=${ coordinates[ 1 ] }`; // Perform the HTTP request to retrieve the weather data let forecast; + let hourlyForecast; try { - forecast = await httpJSONRequest( forecastUrl ); + hourlyForecast = await httpJSONRequest(forecastUrl); + + // The new API call only offers 48 hours of hourly forecast data which is fine because we only use 24 hours + // just need to translate the data into blocks of 3 hours and then use as normal. + // Could probably shortcut this if you knew the following calculations better but less chance of screwing + // up the calculation just by bundling the hourly data into 3 hour blocks so I went that route + if (hourlyForecast && hourlyForecast.hourly) { + forecast = this.get3hForecast(hourlyForecast.hourly, 24); + } + } catch ( err ) { console.error( "Error retrieving weather information from OWM:", err ); throw new CodedError( ErrorCode.WeatherApiError ); @@ -54,62 +65,77 @@ export default class OWMWeatherProvider extends WeatherProvider { }; } - public async getWeatherData( coordinates: GeoCoordinates ): Promise< WeatherData > { - const currentUrl = `http://api.openweathermap.org/data/2.5/weather?appid=${ this.API_KEY }&units=imperial&lat=${ coordinates[ 0 ] }&lon=${ coordinates[ 1 ] }`, - forecastDailyUrl = `http://api.openweathermap.org/data/2.5/forecast/daily?appid=${ this.API_KEY }&units=imperial&lat=${ coordinates[ 0 ] }&lon=${ coordinates[ 1 ] }`; + public async getWeatherData(coordinates: GeoCoordinates): Promise { + // The OWM free API options changed so need to use the new API method + const currentUrl = `https://api.openweathermap.org/data/2.5/weather?appid=${ this.API_KEY }&units=imperial&lat=${ coordinates[ 0 ] }&lon=${ coordinates[ 1 ] }`, + forecastDailyUrl = `https://api.openweathermap.org/data/2.5/onecall?exclude=current,minutely,hourly,alerts&appid=${ this.API_KEY }&units=imperial&lat=${ coordinates[ 0 ] }&lon=${ coordinates[ 1 ] }`; let current, forecast; try { - current = await httpJSONRequest( currentUrl ); - forecast = await httpJSONRequest( forecastDailyUrl ); + forecast = await httpJSONRequest(forecastDailyUrl); + current = await httpJSONRequest(currentUrl); + if (forecast) { + forecast.list = forecast.daily; + forecast.city = { name: current.name, region: current.sys.country }; + } } catch ( err ) { console.error( "Error retrieving weather information from OWM:", err ); throw "An error occurred while retrieving weather information from OWM." } // Indicate watering data could not be retrieved if the forecast data is incomplete. - if ( !current || !current.main || !current.wind || !current.weather || !forecast || !forecast.list ) { + if (!current || !current.main || !current.wind || !current.weather || !forecast || !forecast.list) { throw "Necessary field(s) were missing from weather information returned by OWM."; } const weather: WeatherData = { weatherProvider: "OWM", - temp: parseInt( current.main.temp ), - humidity: parseInt( current.main.humidity ), - wind: parseInt( current.wind.speed ), - description: current.weather[ 0 ].description, - icon: current.weather[ 0 ].icon, + temp: parseInt(current.main.temp), + humidity: parseInt(current.main.humidity), + wind: parseInt(current.wind.speed), + description: current.weather[0].description, + icon: current.weather[0].icon, region: forecast.city.country, city: forecast.city.name, - minTemp: parseInt( forecast.list[ 0 ].temp.min ), - maxTemp: parseInt( forecast.list[ 0 ].temp.max ), - precip: ( forecast.list[ 0 ].rain ? parseFloat( forecast.list[ 0 ].rain || 0 ) : 0 ) / 25.4, + minTemp: parseInt(forecast.list[0].temp.min), + maxTemp: parseInt(forecast.list[0].temp.max), + precip: (forecast.list[0].rain ? parseFloat(forecast.list[0].rain || 0) : 0) / 25.4, forecast: [] }; - for ( let index = 0; index < forecast.list.length; index++ ) { - weather.forecast.push( { - temp_min: parseInt( forecast.list[ index ].temp.min ), - temp_max: parseInt( forecast.list[ index ].temp.max ), - date: parseInt( forecast.list[ index ].dt ), - icon: forecast.list[ index ].weather[ 0 ].icon, - description: forecast.list[ index ].weather[ 0 ].description - } ); + for (let index = 0; index < forecast.list.length; index++) { + weather.forecast.push({ + temp_min: parseInt(forecast.list[index].temp.min), + temp_max: parseInt(forecast.list[index].temp.max), + date: parseInt(forecast.list[index].dt), + icon: forecast.list[index].weather[0].icon, + description: forecast.list[index].weather[0].description + }); } return weather; } // Uses a rolling window since forecast data from further in the future (i.e. the next full day) would be less accurate. - async getEToData( coordinates: GeoCoordinates ): Promise< EToData > { + async getEToData(coordinates: GeoCoordinates): Promise { + // The OWM API changed what you get on the free subscription so need to adjust the call and translate the data. const OWM_API_KEY = process.env.OWM_API_KEY, - forecastUrl = "http://api.openweathermap.org/data/2.5/forecast?appid=" + OWM_API_KEY + "&units=imperial&lat=" + coordinates[ 0 ] + "&lon=" + coordinates[ 1 ]; + forecastUrl = `https://api.openweathermap.org/data/2.5/onecall?exclude=current,minutely,daily,alerts&appid=${ this.API_KEY }&units=imperial&lat=${ coordinates[ 0 ] }&lon=${ coordinates[ 1 ] }`; // Perform the HTTP request to retrieve the weather data let forecast; + let hourlyForecast; try { - forecast = await httpJSONRequest( forecastUrl ); + hourlyForecast = await httpJSONRequest(forecastUrl); + + // The new API call only offers 48 hours of hourly forecast data which is fine because we only use 24 hours + // just need to translate the data into blocks of 3 hours and then use as normal. + // Could probably shortcut this if you knew the following calculations better but less chance of screwing + // up the calculation just by bundling the hourly data into 3 hour blocks so I went that route + if (hourlyForecast && hourlyForecast.hourly) { + forecast = this.get3hForecast(hourlyForecast.hourly, 24); + } } catch (err) { console.error( "Error retrieving ETo information from OWM:", err ); throw new CodedError( ErrorCode.WeatherApiError ); @@ -164,4 +190,103 @@ export default class OWMWeatherProvider extends WeatherProvider { precip: samples.reduce( ( sum, window ) => sum + ( window.rain ? window.rain[ "3h" ] || 0 : 0 ), 0) / 25.4 }; } + + // Expects an array of at least 3 hours of forecast data from the API's onecall method + // Returns the equivilent of the 3 hour object from the previous call from the 5 day forecast API call + public getPeriod3hObject(hourly: any[]) { + + // Should probably define this in a class somewhere but it isn't done with the existing API calls so not bothering + let period3h = { + dt: 0, + main: { + temp: 0.0, + feels_like: 0.0, + temp_min: 0.0, + temp_max: 0.0, + pressure: 0, + sea_level: 0, + grnd_level: 0, + humidity: 0, + temp_kf: 0.0 + }, + weather: [ + { + id: 0, + main: "", + description: "", + icon: "" + } + ], + clouds: { + all: 0 + }, + wind: { + speed: 0.0, + deg: 0, + gust: 0.0 + }, + visibility: 0, + pop: 0.0, + rain: { + "3h": 0.0 + }, + sys: { + pod: "" + }, + dt_txt: "" + }; + + if (hourly && hourly.length > 0 && hourly[2]?.dt) { + + // Could add some more data here if needed, I decided to just minimize the translation work + // Also some of the fields aren't availible in the new call so not worth trying to do a full translation + for (let index = 0; index < 3; index++) { + let hour = hourly[index]; + + period3h.main.temp += hour.temp; + period3h.main.temp_min = period3h.main.temp_min > hour.temp || index == 0 ? hour.temp : period3h.main.temp_min; + period3h.main.temp_max = period3h.main.temp_max < hour.temp || index == 0 ? hour.temp : period3h.main.temp_max; + period3h.main.humidity += hour.humidity; + period3h.wind.speed += hour.wind_speed; + period3h.rain["3h"] += hour.rain == null ? 0.0 : hour.rain["1h"]; + period3h.clouds.all += hour.clouds; + } + + // Some of the decisions could be questionable but I decided to go with the numbers that would err on the side of more + // rather than less + period3h.main.temp = Math.ceil(period3h.main.temp / 3); + period3h.main.humidity = Math.floor(period3h.main.humidity / 3); + period3h.wind.speed = Math.ceil(period3h.wind.speed / 3); + period3h.clouds.all = Math.floor(period3h.clouds.all / 3); + + period3h.dt = hourly[0].dt; + + // Started this and then realized it wasn't used so just left it in + let date = new Date(period3h.dt * 1000); + let month = date.getMonth() + 1; + let day = date.getDate(); + period3h.dt_txt = date.getFullYear() + "-" + ("0" + month.toString()).substring(month > 9 ? 1 : 0) + "-" + ("0" + day.toString()).substring(day > 9 ? 1 : 0) + " " + date.toTimeString().substring(0, 8); + } + } + + // Expects an array of hourly forecast data from the API's onecall method + // Returns a minimally equivilent object to the previous 5 day forecast API call + public get3hForecast(hourly: any[], hours: number = 24) { + + let results = { list: [] }; + + if (!hourly || hourly.length < 3) { + return null; + } + + for (let index = 0; index < hours; index++) { + let hour = hourly[index]; + + if (index % 3 == 0) { + results.list.push(this.getPeriod3hObject(hourly.slice(index))); + } + } + + return results; + } }