From 8368991c67be80c421d8ebbd50a55013686fe1af Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Thu, 6 Jun 2019 16:11:59 -0400 Subject: [PATCH] Convert WeatherProvider from interface to class --- routes/weather.ts | 5 +- routes/weatherProviders/DarkSky.ts | 193 ++++++++++----------- routes/weatherProviders/OWM.ts | 172 +++++++++--------- routes/weatherProviders/WeatherProvider.ts | 19 ++ routes/weatherProviders/local.ts | 43 +++-- types.ts | 18 -- 6 files changed, 222 insertions(+), 228 deletions(-) create mode 100644 routes/weatherProviders/WeatherProvider.ts diff --git a/routes/weather.ts b/routes/weather.ts index c34cf17..257dff8 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -5,8 +5,9 @@ import * as SunCalc from "suncalc"; import * as moment from "moment-timezone"; import * as geoTZ from "geo-tz"; -import { AdjustmentOptions, GeoCoordinates, TimeData, WateringData, WeatherData, WeatherProvider } from "../types"; -const weatherProvider: WeatherProvider = require("./weatherProviders/" + ( process.env.WEATHER_PROVIDER || "OWM" ) ).default; +import { AdjustmentOptions, GeoCoordinates, TimeData, WateringData, WeatherData } from "../types"; +import { WeatherProvider } from "./weatherProviders/WeatherProvider"; +const weatherProvider: WeatherProvider = new ( require("./weatherProviders/" + ( process.env.WEATHER_PROVIDER || "OWM" ) ).default )(); // Define regex filters to match against location const filters = { diff --git a/routes/weatherProviders/DarkSky.ts b/routes/weatherProviders/DarkSky.ts index 8bbe377..7098f39 100644 --- a/routes/weatherProviders/DarkSky.ts +++ b/routes/weatherProviders/DarkSky.ts @@ -1,113 +1,110 @@ import * as moment from "moment-timezone"; -import { GeoCoordinates, WateringData, WeatherData, WeatherProvider } from "../../types"; +import { GeoCoordinates, WateringData, WeatherData } from "../../types"; import { httpJSONRequest } from "../weather"; +import { WeatherProvider } from "./WeatherProvider"; -async function getDarkSkyWateringData( coordinates: GeoCoordinates ): Promise< WateringData > { - // The Unix timestamp of 24 hours ago. - const yesterdayTimestamp: number = moment().subtract( 1, "day" ).unix(); - const todayTimestamp: number = moment().unix(); +export default class DarkSkyWeatherProvider extends WeatherProvider { - const DARKSKY_API_KEY = process.env.DARKSKY_API_KEY, - yesterdayUrl = `https://api.darksky.net/forecast/${DARKSKY_API_KEY}/${coordinates[0]},${coordinates[1]},${yesterdayTimestamp}?exclude=currently,minutely,daily,alerts,flags`, - todayUrl = `https://api.darksky.net/forecast/${DARKSKY_API_KEY}/${coordinates[0]},${coordinates[1]},${todayTimestamp}?exclude=currently,minutely,daily,alerts,flags`; + public async getWateringData( coordinates: GeoCoordinates ): Promise< WateringData > { + // The Unix timestamp of 24 hours ago. + const yesterdayTimestamp: number = moment().subtract( 1, "day" ).unix(); + const todayTimestamp: number = moment().unix(); - let yesterdayData, todayData; - try { - yesterdayData = await httpJSONRequest( yesterdayUrl ); - todayData = await httpJSONRequest( todayUrl ); - } catch (err) { - console.error( "Error retrieving weather information from Dark Sky:", err ); - throw "An error occurred while retrieving weather information from Dark Sky." + const DARKSKY_API_KEY = process.env.DARKSKY_API_KEY, + yesterdayUrl = `https://api.darksky.net/forecast/${ DARKSKY_API_KEY }/${ coordinates[ 0 ] },${ coordinates[ 1 ] },${ yesterdayTimestamp }?exclude=currently,minutely,daily,alerts,flags`, + todayUrl = `https://api.darksky.net/forecast/${ DARKSKY_API_KEY }/${ coordinates[ 0 ] },${ coordinates[ 1 ] },${ todayTimestamp }?exclude=currently,minutely,daily,alerts,flags`; + + let yesterdayData, todayData; + try { + yesterdayData = await httpJSONRequest( yesterdayUrl ); + todayData = await httpJSONRequest( todayUrl ); + } catch ( err ) { + console.error( "Error retrieving weather information from Dark Sky:", err ); + throw "An error occurred while retrieving weather information from Dark Sky." + } + + if ( !todayData.hourly || !todayData.hourly.data || !yesterdayData.hourly || !yesterdayData.hourly.data ) { + throw "Necessary field(s) were missing from weather information returned by Dark Sky."; + } + + /* The number of hourly forecasts to use from today's data. This will only include elements that contain historic + data (not forecast data). */ + // Find the first element that contains forecast data. + const todayElements = Math.min( 24, todayData.hourly.data.findIndex( ( data ) => data.time > todayTimestamp - 60 * 60 ) ); + + /* Take as much data as possible from the first elements of today's data and take the remaining required data from + the remaining data from the last elements of yesterday's data. */ + const samples = [ + ...yesterdayData.hourly.data.slice( todayElements - 24 ), + ...todayData.hourly.data.slice( 0, todayElements ) + ]; + + // Fail if not enough data is available. + if ( samples.length !== 24 ) { + throw "Insufficient data was returned by Dark Sky."; + } + + const totals = { temp: 0, humidity: 0, precip: 0 }; + for ( const sample of samples ) { + totals.temp += sample.temperature; + totals.humidity += sample.humidity; + totals.precip += sample.precipIntensity + } + + return { + weatherProvider: "DarkSky", + temp: totals.temp / 24, + humidity: totals.humidity / 24 * 100, + precip: totals.precip, + raining: samples[ samples.length - 1 ].precipIntensity > 0 + }; } - if ( !todayData.hourly || !todayData.hourly.data || !yesterdayData.hourly || !yesterdayData.hourly.data ) { - throw "Necessary field(s) were missing from weather information returned by Dark Sky."; - } + public async getWeatherData( coordinates: GeoCoordinates ): Promise< WeatherData > { + const DARKSKY_API_KEY = process.env.DARKSKY_API_KEY, + forecastUrl = `https://api.darksky.net/forecast/${ DARKSKY_API_KEY }/${ coordinates[ 0 ] },${ coordinates[ 1 ] }?exclude=minutely,alerts,flags`; - /* The number of hourly forecasts to use from today's data. This will only include elements that contain historic - data (not forecast data). */ - // Find the first element that contains forecast data. - const todayElements = Math.min( 24, todayData.hourly.data.findIndex( ( data ) => data.time > todayTimestamp - 60 * 60 ) ); + let forecast; + try { + forecast = await httpJSONRequest( forecastUrl ); + } catch ( err ) { + console.error( "Error retrieving weather information from Dark Sky:", err ); + throw "An error occurred while retrieving weather information from Dark Sky." + } - /* Take as much data as possible from the first elements of today's data and take the remaining required data from - the remaining data from the last elements of yesterday's data. */ - const samples = [ - ...yesterdayData.hourly.data.slice( todayElements - 24 ), - ...todayData.hourly.data.slice( 0, todayElements ) - ]; + if ( !forecast.currently || !forecast.daily || !forecast.daily.data ) { + throw "Necessary field(s) were missing from weather information returned by Dark Sky."; + } - // Fail if not enough data is available. - if ( samples.length !== 24 ) { - throw "Insufficient data was returned by Dark Sky."; - } - - const totals = { temp: 0, humidity: 0, precip: 0 }; - for ( const sample of samples ) { - totals.temp += sample.temperature; - totals.humidity += sample.humidity; - totals.precip += sample.precipIntensity - } - - return { - weatherProvider: "DarkSky", - temp : totals.temp / 24, - humidity: totals.humidity / 24 * 100, - precip: totals.precip, - raining: samples[ samples.length - 1 ].precipIntensity > 0 - }; -} - -async function getDarkSkyWeatherData( coordinates: GeoCoordinates ): Promise< WeatherData > { - const DARKSKY_API_KEY = process.env.DARKSKY_API_KEY, - forecastUrl = `https://api.darksky.net/forecast/${DARKSKY_API_KEY}/${coordinates[0]},${coordinates[1]}?exclude=minutely,alerts,flags`; - - let forecast; - try { - forecast = await httpJSONRequest( forecastUrl ); - } catch (err) { - console.error( "Error retrieving weather information from Dark Sky:", err ); - throw "An error occurred while retrieving weather information from Dark Sky." - } - - if ( !forecast.currently || !forecast.daily || !forecast.daily.data ) { - throw "Necessary field(s) were missing from weather information returned by Dark Sky."; - } - - const weather: WeatherData = { - weatherProvider: "DarkSky", - temp: Math.floor( forecast.currently.temperature ), - humidity: Math.floor( forecast.currently.humidity * 100 ), - wind: Math.floor( forecast.currently.windSpeed ), - description: forecast.currently.summary, - // TODO set this - icon: "", - - region: "", - city: "", - minTemp: Math.floor( forecast.daily.data[ 0 ].temperatureMin ), - maxTemp: Math.floor( forecast.daily.data[ 0 ].temperatureMax ), - precip: forecast.daily.data[ 0 ].precipIntensity * 24, - forecast: [ ] - }; - - for ( let index = 0; index < forecast.daily.data.length; index++ ) { - weather.forecast.push( { - temp_min: Math.floor( forecast.daily.data[ index ].temperatureMin ), - temp_max: Math.floor( forecast.daily.data[ index ].temperatureMax ), - date: forecast.daily.data[ index ].time, + const weather: WeatherData = { + weatherProvider: "DarkSky", + temp: Math.floor( forecast.currently.temperature ), + humidity: Math.floor( forecast.currently.humidity * 100 ), + wind: Math.floor( forecast.currently.windSpeed ), + description: forecast.currently.summary, // TODO set this icon: "", - description: forecast.daily.data[ index ].summary - } ); + + region: "", + city: "", + minTemp: Math.floor( forecast.daily.data[ 0 ].temperatureMin ), + maxTemp: Math.floor( forecast.daily.data[ 0 ].temperatureMax ), + precip: forecast.daily.data[ 0 ].precipIntensity * 24, + forecast: [] + }; + + for ( let index = 0; index < forecast.daily.data.length; index++ ) { + weather.forecast.push( { + temp_min: Math.floor( forecast.daily.data[ index ].temperatureMin ), + temp_max: Math.floor( forecast.daily.data[ index ].temperatureMax ), + date: forecast.daily.data[ index ].time, + // TODO set this + icon: "", + description: forecast.daily.data[ index ].summary + } ); + } + + return weather; } - - return weather; } - - -const DarkSkyWeatherProvider: WeatherProvider = { - getWateringData: getDarkSkyWateringData, - getWeatherData: getDarkSkyWeatherData -}; -export default DarkSkyWeatherProvider; diff --git a/routes/weatherProviders/OWM.ts b/routes/weatherProviders/OWM.ts index af86c48..fdb4023 100644 --- a/routes/weatherProviders/OWM.ts +++ b/routes/weatherProviders/OWM.ts @@ -1,94 +1,92 @@ -import { GeoCoordinates, WateringData, WeatherData, WeatherProvider } from "../../types"; +import { GeoCoordinates, WateringData, WeatherData } from "../../types"; import { httpJSONRequest } from "../weather"; +import { WeatherProvider } from "./WeatherProvider"; -async function getOWMWateringData( coordinates: GeoCoordinates ): Promise< WateringData > { - 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 ]; +export default class OWMWeatherProvider extends WeatherProvider { - // Perform the HTTP request to retrieve the weather data - let forecast; - try { - forecast = await httpJSONRequest( forecastUrl ); - } catch (err) { - console.error( "Error retrieving weather information from OWM:", err ); - throw "An error occurred while retrieving weather information from OWM." + public async getWateringData( coordinates: GeoCoordinates ): Promise< WateringData > { + 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 ]; + + // Perform the HTTP request to retrieve the weather data + let forecast; + try { + forecast = await httpJSONRequest( forecastUrl ); + } 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 ( !forecast || !forecast.list ) { + throw "Necessary field(s) were missing from weather information returned by OWM."; + } + + let totalTemp = 0, + totalHumidity = 0, + totalPrecip = 0; + + const periods = Math.min( forecast.list.length, 8 ); + for ( let index = 0; index < periods; index++ ) { + totalTemp += parseFloat( forecast.list[ index ].main.temp ); + totalHumidity += parseInt( forecast.list[ index ].main.humidity ); + totalPrecip += ( forecast.list[ index ].rain ? parseFloat( forecast.list[ index ].rain[ "3h" ] || 0 ) : 0 ); + } + + return { + weatherProvider: "OWM", + temp: totalTemp / periods, + humidity: totalHumidity / periods, + precip: totalPrecip / 25.4, + raining: ( forecast.list[ 0 ].rain ? ( parseFloat( forecast.list[ 0 ].rain[ "3h" ] || 0 ) > 0 ) : false ) + }; } - // Indicate watering data could not be retrieved if the forecast data is incomplete. - if ( !forecast || !forecast.list ) { - throw "Necessary field(s) were missing from weather information returned by OWM."; + public async getWeatherData( coordinates: GeoCoordinates ): Promise< WeatherData > { + const OWM_API_KEY = process.env.OWM_API_KEY, + currentUrl = "http://api.openweathermap.org/data/2.5/weather?appid=" + OWM_API_KEY + "&units=imperial&lat=" + coordinates[ 0 ] + "&lon=" + coordinates[ 1 ], + forecastDailyUrl = "http://api.openweathermap.org/data/2.5/forecast/daily?appid=" + OWM_API_KEY + "&units=imperial&lat=" + coordinates[ 0 ] + "&lon=" + coordinates[ 1 ]; + + let current, forecast; + try { + current = await httpJSONRequest( currentUrl ); + forecast = await httpJSONRequest( forecastDailyUrl ); + } 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 ) { + 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, + + 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, + 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 + } ); + } + + return weather; } - - let totalTemp = 0, - totalHumidity = 0, - totalPrecip = 0; - - const periods = Math.min(forecast.list.length, 8); - for ( let index = 0; index < periods; index++ ) { - totalTemp += parseFloat( forecast.list[ index ].main.temp ); - totalHumidity += parseInt( forecast.list[ index ].main.humidity ); - totalPrecip += ( forecast.list[ index ].rain ? parseFloat( forecast.list[ index ].rain[ "3h" ] || 0 ) : 0 ); - } - - return { - weatherProvider: "OWM", - temp: totalTemp / periods, - humidity: totalHumidity / periods, - precip: totalPrecip / 25.4, - raining: ( forecast.list[ 0 ].rain ? ( parseFloat( forecast.list[ 0 ].rain[ "3h" ] || 0 ) > 0 ) : false ) - }; } - -async function getOWMWeatherData( coordinates: GeoCoordinates ): Promise< WeatherData > { - const OWM_API_KEY = process.env.OWM_API_KEY, - currentUrl = "http://api.openweathermap.org/data/2.5/weather?appid=" + OWM_API_KEY + "&units=imperial&lat=" + coordinates[ 0 ] + "&lon=" + coordinates[ 1 ], - forecastDailyUrl = "http://api.openweathermap.org/data/2.5/forecast/daily?appid=" + OWM_API_KEY + "&units=imperial&lat=" + coordinates[ 0 ] + "&lon=" + coordinates[ 1 ]; - - let current, forecast; - try { - current = await httpJSONRequest( currentUrl ); - forecast = await httpJSONRequest( forecastDailyUrl ); - } 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 ) { - 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, - - 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, - 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 - } ); - } - - return weather; -} - -const OWMWeatherProvider: WeatherProvider = { - getWateringData: getOWMWateringData, - getWeatherData: getOWMWeatherData -}; -export default OWMWeatherProvider; diff --git a/routes/weatherProviders/WeatherProvider.ts b/routes/weatherProviders/WeatherProvider.ts new file mode 100644 index 0000000..5581860 --- /dev/null +++ b/routes/weatherProviders/WeatherProvider.ts @@ -0,0 +1,19 @@ +import { GeoCoordinates, WateringData, WeatherData } from "../../types"; + +export class WeatherProvider { + /** + * Retrieves weather data necessary for watering level calculations. + * @param coordinates The coordinates to retrieve the watering data for. + * @return A Promise that will be resolved with the WateringData if it is successfully retrieved, + * or rejected with an error message if an error occurs while retrieving the WateringData. + */ + getWateringData?( coordinates : GeoCoordinates ): Promise< WateringData >; + + /** + * Retrieves the current weather data for usage in the mobile app. + * @param coordinates The coordinates to retrieve the weather for + * @return A Promise that will be resolved with the WeatherData if it is successfully retrieved, + * or rejected with an error message if an error occurs while retrieving the WeatherData. + */ + getWeatherData?( coordinates : GeoCoordinates ): Promise< WeatherData >; +} diff --git a/routes/weatherProviders/local.ts b/routes/weatherProviders/local.ts index 314df1c..71e4798 100644 --- a/routes/weatherProviders/local.ts +++ b/routes/weatherProviders/local.ts @@ -1,6 +1,7 @@ import * as express from "express"; import { CronJob } from "cron"; -import { GeoCoordinates, WateringData, WeatherProvider } from "../../types"; +import { GeoCoordinates, WateringData } from "../../types"; +import { WeatherProvider } from "./WeatherProvider"; const count = { temp: 0, humidity: 0 }; @@ -43,22 +44,25 @@ export const captureWUStream = function( req: express.Request, res: express.Resp res.send( "success\n" ); }; -export const getLocalWateringData = function(): WateringData { - const result: WateringData = { - ...yesterday as WateringData, - // Use today's weather if we dont have information for yesterday yet (i.e. on startup) - ...today, - // PWS report "buckets" so consider it still raining if last bucket was less than an hour ago - raining: last_bucket !== undefined ? ( ( Date.now() - +last_bucket ) / 1000 / 60 / 60 < 1 ) : undefined, - weatherProvider: "local" +export default class LocalWeatherProvider extends WeatherProvider { + + public async getWateringData( coordinates: GeoCoordinates ): Promise< WateringData > { + const result: WateringData = { + ...yesterday as WateringData, + // Use today's weather if we dont have information for yesterday yet (i.e. on startup) + ...today, + // PWS report "buckets" so consider it still raining if last bucket was less than an hour ago + raining: last_bucket !== undefined ? ( ( Date.now() - +last_bucket ) / 1000 / 60 / 60 < 1 ) : undefined, + weatherProvider: "local" + }; + + if ( "precip" in yesterday && "precip" in today ) { + result.precip = yesterday.precip + today.precip; + } + + return result; }; - - if ( "precip" in yesterday && "precip" in today ) { - result.precip = yesterday.precip + today.precip; - } - - return result; -}; +} new CronJob( "0 0 0 * * *", function() { @@ -74,10 +78,3 @@ interface PWSStatus { humidity?: number; precip?: number; } - -const LocalWeatherProvider: WeatherProvider = { - getWateringData: async function ( coordinates: GeoCoordinates ) { - return getLocalWateringData(); - } -}; -export default LocalWeatherProvider; diff --git a/types.ts b/types.ts index 8fcdf97..07bbadb 100644 --- a/types.ts +++ b/types.ts @@ -85,22 +85,4 @@ export interface AdjustmentOptions { d?: number; } -export interface WeatherProvider { - /** - * Retrieves weather data necessary for watering level calculations. - * @param coordinates The coordinates to retrieve the watering data for. - * @return A Promise that will be resolved with the WateringData if it is successfully retrieved, - * or rejected with an error message if an error occurs while retrieving the WateringData. - */ - getWateringData?( coordinates : GeoCoordinates ): Promise< WateringData >; - - /** - * Retrieves the current weather data for usage in the mobile app. - * @param coordinates The coordinates to retrieve the weather for - * @return A Promise that will be resolved with the WeatherData if it is successfully retrieved, - * or rejected with an error message if an error occurs while retrieving the WeatherData. - */ - getWeatherData?( coordinates : GeoCoordinates ): Promise< WeatherData >; -} - export type WeatherProviderId = "OWM" | "DarkSky" | "local";