UNPKG

jw-weather

Version:

Reader for various weather APIs

557 lines (480 loc) 22.6 kB
/* ver 2.1.2 -minor bugfix ver 2.1.1 -remove hard-coded key to weatherbit ver 2.1.0 -Add OpenWetherMap support -Add WeatherBit support -Complete rewrite -require jw-gate ver 1.0.2 -includes celsius option */ var jwGate = require('jw-gate'); /** * Creates a service for various online weather APIs * * @param {Object} options * @param {String} options.key - API Key * @param {'darksky'|'accuweather'|'openweathermap'|weatherbit} options.provider - The weather provider to use * @param {Number} [options.latitude] - The latitude * @param {Number} [options.longitude] - the longitude * @param {boolean} [options.celsius] - Temperature in celsius * */ function service(options) { var _self = this; // weather information this.raw = {}; //this is the raw API response from the provider //an object to hold the various data elements and urls to retrieve the data this.weatherData = {}; this.lastUpdate = null; this.forecastTime = null; this.temp = null; this.humidity = null; this.currentCondition = null; this.icon = null; this.feelsLike = null; this.sunrise = null; this.sunset = null; this.error = null; this.forecast = []; this.ready = false; //this is for weather services that need to do lookups this.runUpdateWhenReady = false; //this is in case the user requests an update before the weather object is ready this.updateWhenReadyFunc = null; //will hold the callback for the early update request this.locationKey = null; //this is for accuweather /******************* Custom Emitter Code **************************************************/ //this is for future browser compatibility var _events = {}; this.on = function(event, callback) { //attaches a callback function to an event _events[event] = callback; }; function emit(event, msg) { if (typeof _events[event] === 'function') { //the client has registered the event _events[event](msg); //run the event function provided } } /*******************************************************************************************/ (function startup() { if (typeof options.key !== 'string' || typeof options.latitude !== 'number' || typeof options.longitude !== 'number') { setTimeout(function() { emit('error', 'Invalid startup options.'); }, 100); } options.provider = options.provider.toLowerCase(); // TODO: Actually test that darksky is working before ready? Maybe just ping the service? if (options.provider === 'darksky' || options.provider === 'openweathermap' || options.provider === 'weatherbit') { _self.ready = true; if (_self.runUpdateWhenReady) { _self.update(_self.updateWhenReadyFunc); } setTimeout(function() { emit('ready', null); }, 100); } else if (options.provider === 'accuweather') { accuweatherLocationLookup(options, function(err, locationKey) { _self.locationKey = locationKey; _self.ready = true; if (_self.runUpdateWhenReady) { _self.update(_self.updateWhenReadyFunc); } setTimeout(function() { emit('ready', null); }, 100); }); } })(); /******* PUBLIC FUNCTIONS *******************************************/ /** * Updates the weather data * @param {Function} [callback] */ this.update = function(callback) { if (_self.ready) { getWeatherData(callback); } else { _self.runUpdateWhenReady = true; _self.updateWhenReadyFunc = callback; } }; /** * @returns {Object} - an object representing the top-level vars */ this.fullWeather = function() { //console.log('returning full weather'); var weather = { lastUpdate: _self.lastUpdate, temp: _self.temp, humidity: _self.humidity, currentCondition: _self.currentCondition, feelsLike: _self.feelsLike, sunrise: _self.sunrise, sunset: _self.sunset, forecastTime: _self.forecastTime, forecast: _self.forecast, icon: _self.icon }; return weather; }; /******* END PUBLIC FUNCTIONS *******************************************/ function getWeatherData(callback) { /* This function is perhaps to clever for it's own good. This is bad. I will attempt to explain the design: Each weather provider has 1 or more different API calls that are required to retrieve the standard data. 1) Create a weather object with all of the urls needed. 2) Create a jw-gate object using the weather object's keys as lock names jw-gate is a simple class for running sync events. When all the "locks" on the gate become unlocked the gate itself becomes unlocked and whatever needs to happen after all of the sync events are completed happens. 3) Iterate through the weather object and get the data. This step is the confusing one because after the data is retrieved the url in the weather object will be replaced with the data itself, so in simple terms: weather { Forecast: 'https://API.com' } becomes: weather { ForeCast: { temp: 25.45, humidity: 48 } } 4) Parse the weather data using a unique parser for that API 5) Run the callback function */ //will be null if the parser is successful or will contain an error var err = null; //fill the weatherData object with the needed API calls if (options.provider === 'openweathermap') { weatherData = { CurrentConditions: 'https://api.openweathermap.org/data/2.5/weather?units=imperial&lat=[LAT]&lon=[LONG]&appid=[KEY]', Forecast: 'https://api.openweathermap.org/data/2.5/forecast?units=imperial&lat=[LAT]&lon=[LONG]&appid=[KEY]' }; } else if (options.provider === 'accuweather') { weatherData = { CurrentConditions: 'https://dataservice.accuweather.com/currentconditions/v1/[LOCATION]?apikey=[KEY]&details=true', Forecast: 'https://dataservice.accuweather.com/forecasts/v1/daily/5day/[LOCATION]?apikey=[KEY]&details=true' }; } else if (options.provider === 'darksky') { weatherData = { Forecast: 'https://api.darksky.net/forecast/[KEY]/[LAT],[LONG]?exclude=["minutely","flags","alerts"]' }; } else if (options.provider === 'weatherbit') { weatherData = { Forecast: 'https://api.weatherbit.io/v2.0/forecast/daily?lat=[LAT]&lon=[LONG]&days=5&units=I&key=[KEY]', CurrentConditions: 'https://api.weatherbit.io/v2.0/current?lat=40.758556&lon=-73.765434&units=I&key=[KEY]' }; } //get an array holding the weatherData keys var apiCalls = Object.keys(weatherData); //create a gate to allow for simultaneous API calls using the keys var apiGate = new jwGate.Gate(apiCalls, true); //this will fire when all the API calls are complete apiGate.on('unlocked', function() { if (options.provider === 'openweathermap') { err = parseOpenWeatherMap(); } else if (options.provider === 'accuweather') { err = parseAccuweather(); } else if (options.provider === 'darksky') { err = parseDarkSky(); } else if (options.provider === 'weatherbit') { err = parseWeatherBit(); } //emit the error if it has been set if (err) { emit('error', err); } //run the callback function if (typeof callback === 'function') { callback(err); } }); //replace the values in the url and get the data apiCalls.forEach(function(url) { weatherData[url] = weatherData[url].replace('[KEY]',options.key) .replace('[LAT]', options.latitude.toString()) .replace('[LONG]', options.longitude.toString()) .replace('[LOCATION]', _self.locationKey); //console.log(weatherData[url]); getAPIData(weatherData[url], function(err, weather) { if (err) { callback(err); } else { //this replaces the url with the weather data retrieved from the API weatherData[url] = weather; apiGate.lock(url, false); } }); }); } function getAPIData(url, callback) { var https = require('https'); try { https.get(url, function(res){ var body = ''; res.on('data', function(chunk){ body += chunk; }); res.on('end', function(){ try { body = JSON.parse(body); //console.log(body); if (typeof callback === 'function') { callback(null, body); } } catch (err) { if (typeof callback === 'function') { callback(err); } } }); }).on('error', function(err) { if (typeof callback === 'function') { callback(err); } }); } catch (err) { if (typeof callback === 'function') { callback(err); } } } function parseDarkSky() { _self.lastUpdate = new Date().toString(); try { var weather = weatherData.Forecast; if (weather.error) { if (typeof callback === 'function') {callback(weather.error); } } else { _self.raw = weather; _self.temp = weather.currently.temperature.toFixed(2); _self.feelsLike = weather.currently.apparentTemperature.toFixed(2); _self.currentCondition = weather.currently.summary; _self.humidity = weather.currently.humidity.toFixed(2); _self.forecastTime = new Date(weather.currently.time * 1000); _self.sunrise = new Date(weather.daily.data[0].sunriseTime * 1000); _self.sunset = new Date(weather.daily.data[0].sunsetTime * 1000); _self.icon = weather.currently.icon; //set celsius if desired if (options.celsius) { _self.temp = toCelsius(_self.temp); _self.feelsLike = toCelsius(_self.feelsLike); } //create the forecast for (var i=0; i<5; i++) { var day = getDailyObject(); day.condition = weather.daily.data[i].summary; day.feelsLikeHigh = weather.daily.data[i].apparentTemperatureHigh.toFixed(2); day.feelsLikeLow = weather.daily.data[i].apparentTemperatureLow.toFixed(2); day.humidity = weather.daily.data[i].humidity; day.icon = weather.daily.data[i].icon; day.sunrise = new Date(weather.daily.data[i].sunriseTime * 1000); day.sunset = new Date(weather.daily.data[i].sunsetTime * 1000); day.tempHigh = weather.daily.data[i].temperatureHigh.toFixed(2); day.tempLow = weather.daily.data[i].temperatureLow.toFixed(2); if (options.celsius) { day.feelsLikeHigh = toCelsius(day.feelsLikeHigh); day.feelsLikeLow = toCelsius(day.feelsLikeLow); day.tempHigh = toCelsius(day.tempHigh); day.tempLow = toCelsius(day.tempLow); } _self.forecast.push(day); } return null; } } catch (err) { _self.error = err; return err; } } function parseAccuweather() { _self.lastUpdate = new Date().toString(); _self.raw = weatherData; try { var CurrentConditions = weatherData.CurrentConditions[0]; //break it out of the array var Forecast = weatherData.Forecast; _self.temp = CurrentConditions.Temperature.Imperial.Value.toFixed(2); _self.feelsLike = CurrentConditions.RealFeelTemperature.Imperial.Value.toFixed(2); _self.currentCondition = CurrentConditions.WeatherText; _self.humidity = (CurrentConditions.RelativeHumidity/100).toFixed(2); _self.forecastTime = new Date(CurrentConditions.LocalObservationDateTime); _self.sunrise = new Date(Forecast.DailyForecasts[0].Sun.Rise); _self.sunset = new Date(Forecast.DailyForecasts[0].Sun.Set); _self.icon = CurrentConditions.WeatherIcon; //set celsius if desired if (options.celsius) { _self.temp = toCelsius(_self.temp); _self.feelsLike = toCelsius(_self.feelsLike); } //create the forecast for (var i=0; i<5; i++) { var day = getDailyObject(); day.condition = Forecast.DailyForecasts[i].Day.ShortPhrase; day.feelsLikeHigh = Forecast.DailyForecasts[i].RealFeelTemperature.Maximum.Value.toFixed(2); day.feelsLikeLow = Forecast.DailyForecasts[i].RealFeelTemperature.Minimum.Value.toFixed(2); day.humidity = null; day.icon = Forecast.DailyForecasts[i].Day.Icon; day.sunrise = new Date(Forecast.DailyForecasts[i].Sun.Rise); day.sunset = new Date(Forecast.DailyForecasts[i].Sun.Set); day.tempHigh = Forecast.DailyForecasts[i].Temperature.Maximum.Value.toFixed(2); day.tempLow = Forecast.DailyForecasts[i].Temperature.Minimum.Value.toFixed(2); if (options.celsius) { day.feelsLikeHigh = toCelsius(day.feelsLikeHigh); day.feelsLikeLow = toCelsius(day.feelsLikeLow); day.tempHigh = toCelsius(day.tempHigh); day.tempLow = toCelsius(day.tempLow); } _self.forecast.push(day); } return null; } catch (err) { _self.error = err; return err; } } function parseOpenWeatherMap() { _self.lastUpdate = new Date().toString(); try { var CurrentConditions = weatherData.CurrentConditions; var Forecast = weatherData.Forecast; _self.raw = weatherData; _self.temp = CurrentConditions.main.temp.toFixed(2); _self.feelsLike = CurrentConditions.main.feels_like.toFixed(2); _self.currentCondition = CurrentConditions.weather[0].description; _self.humidity = CurrentConditions.main.humidity.toFixed(2); //_self.forecastTime = new Date(CurrentConditions * 1000); _self.sunrise = new Date(CurrentConditions.sys.sunrise * 1000); _self.sunset = new Date(CurrentConditions.sys.sunset * 1000); _self.icon = CurrentConditions.weather[0].icon; //set celsius if desired if (options.celsius) { _self.temp = toCelsius(_self.temp); _self.feelsLike = toCelsius(_self.feelsLike); } //create the forecast for (var i=0; i<5; i++) { var day = getDailyObject(); day.condition = Forecast.list[i].weather[0].description; day.feelsLikeHigh = Forecast.list[i].main.temp_max.toFixed(2); day.feelsLikeLow = Forecast.list[i].main.temp_min.toFixed(2); day.humidity = Forecast.list[i].main.humidity.toFixed(2); day.icon = null; day.sunrise = null; day.sunset = null; day.tempHigh = day.feelsLikeHigh; day.tempLow =day.feelsLikeLow; if (options.celsius) { day.feelsLikeHigh = toCelsius(day.feelsLikeHigh); day.feelsLikeLow = toCelsius(day.feelsLikeLow); day.tempHigh = toCelsius(day.tempHigh); day.tempLow = toCelsius(day.tempLow); } _self.forecast.push(day); } return null; } catch (err) { _self.error = err; return err; } } function parseWeatherBit() { _self.lastUpdate = new Date().toString(); try { _self.raw = weatherData; var CurrentConditions = weatherData.CurrentConditions.data[0]; var Forecast = weatherData.Forecast; _self.temp = CurrentConditions.temp.toFixed(2); _self.feelsLike = CurrentConditions.app_temp.toFixed(2); _self.currentCondition = CurrentConditions.weather.description; _self.humidity = CurrentConditions.rh.toFixed(2); _self.forecastTime = new Date(CurrentConditions.ts * 1000); _self.sunrise = new Date(Forecast.data[0].sunrise_ts * 1000); _self.sunset = new Date(Forecast.data[0].sunset_ts * 1000); _self.icon = CurrentConditions.weather.icon; //set celsius if desired if (options.celsius) { _self.temp = toCelsius(_self.temp); _self.feelsLike = toCelsius(_self.feelsLike); } //create the forecast for (var i=0; i<5; i++) { var day = getDailyObject(); day.condition = Forecast.data[i].weather.description; day.feelsLikeHigh = Forecast.data[i].app_max_temp.toFixed(2); day.feelsLikeLow = Forecast.data[i].app_min_temp.toFixed(2); day.humidity = Forecast.data[i].rh.toFixed(2); day.icon = Forecast.data[i].weather.icon; day.sunrise = new Date(Forecast.data[i].sunrise_ts * 1000); day.sunset = new Date(Forecast.data[i].sunset_ts * 1000); day.tempHigh = Forecast.data[i].max_temp.toFixed(2); day.tempLow = Forecast.data[i].low_temp.toFixed(2); if (options.celsius) { day.feelsLikeHigh = toCelsius(day.feelsLikeHigh); day.feelsLikeLow = toCelsius(day.feelsLikeLow); day.tempHigh = toCelsius(day.tempHigh); day.tempLow = toCelsius(day.tempLow); } _self.forecast.push(day); } return null; } catch (err) { _self.error = err; return err; } } } ///END OF service object /** * Looks up the location key needed for Accuweather * * @param {object} options * @param {String} options.key - API Key * @param {number|string} [options.latitude] - The latitude * @param {number|string} [options.longitude] - the longitude * @param {function} callback */ function accuweatherLocationLookup(options, callback) { var err = null; if (typeof options.key !== 'string') { err = 'Invalid startup options.'; if (typeof callback === 'function') { callback(err); } return null; } var https = require('https'); var url = ''; var mode = null; if (options.latitude && options.longitude) { url = 'https://dataservice.accuweather.com/locations/v1/cities/geoposition/search?apikey=[KEY]&q=[LAT],[LONG]'; } else { err = 'Invalid location information'; if (typeof callback === 'function') { callback(err); } return null; } url = url.replace('[KEY]',options.key) .replace('[LAT]', options.latitude) .replace('[LONG]', options.longitude); try { https.get(url, function(res){ var body = ''; res.on('data', function(chunk){ body += chunk; }); res.on('end', function(){ try { var res = JSON.parse(body); var locationKey = ''; locationKey = res.Key; if (typeof callback === 'function') { callback(null, locationKey); } } catch (err) { if (typeof callback === 'function') { callback(err); } } }); }).on('error', function(err) { if (typeof callback === 'function') { callback(err); } }); } catch (err) { if (typeof callback === 'function') { callback(err); } } } function toCelsius(temp) { return ((temp-32)*(5/9)).toFixed(2); } function getDailyObject() { var daily = { tempHigh: null, tempLow: null, humidity: null, condition: null, feelsLikeHigh: null, feelsLikeLow: null, sunrise: null, sunset: null, icon: null }; return daily; } exports.service = service;