diff --git a/routes/weatherProviders/OWM.ts b/routes/weatherProviders/OWM.ts index 9aa169c..52fe85d 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 skip this but less chance of changing the output this way + 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,75 @@ 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=" + OWM_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); + + // translating the hourly into a 3h forecast again could probably ditch the translation + // but to be safe just sticking with the 3h translation + 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 +188,98 @@ 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 an aggregated object for the first 3 hours of the hourly array, should be equivalent to the + // 3 hour object from the 5 day forecast + getPeriod3hObject(hourly: any[]) { + + 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 > 2 && hourly[2].dt) { + + // 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; + } + + // Defaulting to floor to err on the side of more watering + period3h.main.temp = period3h.main.temp / 3; + period3h.main.humidity = Math.floor(period3h.main.humidity / 3); + period3h.wind.speed = period3h.wind.speed / 3; + period3h.clouds.all = Math.floor(period3h.clouds.all / 3); + + period3h.dt = hourly[0].dt; + } + + return period3h; + } + + // Expects an array of hourly forecast data from the API's onecall method + // Returns a minimally equivalent object to the previous 5 day forecast API call + 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))); + } + } + + // returning null should give a better error message if there no data from the service + return results.list.length > 0 ? results : null; + } }