From 664cc3666a39cbdbda365a01a166e55af86c773d Mon Sep 17 00:00:00 2001 From: Samer Albahra Date: Wed, 1 Jul 2015 18:45:27 -0500 Subject: [PATCH] Wrap code within a closure function --- routes/weather.js | 519 +++++++++++++++++++++++----------------------- 1 file changed, 263 insertions(+), 256 deletions(-) diff --git a/routes/weather.js b/routes/weather.js index ce6f769..0dd0aa6 100644 --- a/routes/weather.js +++ b/routes/weather.js @@ -1,271 +1,278 @@ -// Define regex filters to match against location -var http = require( "http" ), - filters = { - gps: /^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/, - pws: /^(?:pws|icao):/, - url: /^https?:\/\/([\w\.-]+)(:\d+)?(\/.*)?$/, - time: /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})([+-])(\d{2})(\d{2})/ - }; +( function() { -// Generic HTTP request handler that parses the URL and uses the -// native Node.js http module to perform the request -function httpRequest( url, callback ) { - url = url.match( filters.url ); - - var options = { - host: url[1], - port: url[2] || 80, - path: url[3] - }; - - http.get( options, function( response ) { - var data = ""; - - // Reassemble the data as it comes in - response.on( "data", function( chunk ) { - data += chunk; - } ); - - // Once the data is completely received, return it to the callback - response.on( "end", function() { - callback( data ); - } ); - } ).on( "error", function() { - - // If the HTTP request fails, return false - callback( false ); - } ); -} - -// Converts IP string to integer -function ipToInt( ip ) { - ip = ip.split( "." ); - return ( ( ( ( ( ( +ip[0] ) * 256 ) + ( +ip[1] ) ) * 256 ) + ( +ip[2] ) ) * 256 ) + ( +ip[3] ); -} - -// Takes a PWS or ICAO location and resolves the GPS coordinates -function getPWSCoordinates( location, weatherUndergroundKey, callback ) { - var url = "http://api.wunderground.com/api/" + weatherUndergroundKey + - "/conditions/forecast/q/" + encodeURIComponent( location ) + ".json"; - - httpRequest( url, function( data ) { - data = JSON.parse( data ); - - if ( typeof data === "object" && data.current_observation && data.current_observation.observation_location ) { - callback( [ data.current_observation.observation_location.latitude, - data.current_observation.observation_location.longitude ] ); - } else { - callback( false ); - } - } ); -} - -// If location does not match GPS or PWS/ICAO, then attempt to resolve -// location using Weather Underground autocomplete API -function resolveCoordinates( location, callback ) { - var url = "http://autocomplete.wunderground.com/aq?h=0&query=" + - encodeURIComponent( location ); - - httpRequest( url, function( data ) { - data = JSON.parse( data ); - if ( typeof data.RESULTS === "object" && data.RESULTS.length ) { - callback( [ data.RESULTS[0].lat, data.RESULTS[0].lon ] ); - } - } ); -} - -// Accepts a time string formatted in ISO-8601 and returns the timezone. -// The timezone output is formatted for OpenSprinkler Unified firmware. -function getTimezone( time ) { - time = time.match( filters.time ); - - var hour = parseInt( time[7] + time[8] ), - minute = parseInt( time[9] ); - - minute = ( minute / 15 >> 0 ) / 4.0; - hour = hour + ( hour >=0 ? minute : -minute ); - - return ( ( hour + 12 ) * 4 ) >> 0; -} - -// Retrieve weather data to complete the weather request -function getWeatherData( location, callback ) { - - // Get the API key from the environment variables - var WSI_API_KEY = process.env.WSI_API_KEY, - - // Generate URL using The Weather Company API v1 in Imperial units - url = "http://api.weather.com/v1/geocode/" + location[0] + "/" + location[1] + - "/observations/current.json?apiKey=" + WSI_API_KEY + "&language=en-US&units=e"; - - // Perform the HTTP request to retrieve the weather data - httpRequest( url, function( data ) { - callback( JSON.parse( data ) ); - } ); -} - -// Calculates the resulting water scale using the provided weather data, adjustment method and options -function calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ) { - - // Calculate the average temperature - var temp = ( weather.observation.imperial.temp_max_24hour + weather.observation.imperial.temp_min_24hour ) / 2, - - // Relative humidity and if unavailable default to 0 - rh = weather.observation.imperial.rh || 0, - - // The absolute precipitation in the past 48 hours - precip = weather.observation.imperial.precip_2day || weather.observation.imperial.precip_24hour; - - if ( typeof temp !== "number" ) { - - // If the maximum and minimum temperatures are not available then use the current temperature - temp = weather.observation.imperial.temp; - } - - // Zimmerman method - if ( adjustmentMethod == 1 ) { - - var humidityFactor = ( 30 - rh ), - tempFactor = ( ( temp - 70 ) * 4 ), - precipFactor = ( precip * -2 ); - - // Apply adjustment options if available - if ( adjustmentOptions ) { - if ( adjustmentOptions.hasOwnProperty( "h" ) ) { - humidityFactor = humidityFactor * ( adjustmentOptions.h / 100 ); - } - - if ( adjustmentOptions.hasOwnProperty( "t" ) ) { - tempFactor = tempFactor * ( adjustmentOptions.t / 100 ); - } - - if ( adjustmentOptions.hasOwnProperty( "r" ) ) { - precipFactor = precipFactor * ( adjustmentOptions.r / 100 ); - } - } - - return parseInt( Math.min( Math.max( 0, 100 + humidityFactor + tempFactor + precipFactor ), 200 ) ); - } - - return -1; -} - -// Function to return the sunrise and sunset times from the weather reply -function getSunData( weather ) { - - // Sun times must be converted from strings into date objects and processed into minutes from midnight - var sunrise = weather.observation.sunrise.match( filters.time ), - sunset = weather.observation.sunset.match( filters.time ); - - return [ - parseInt( sunrise[4] ) * 60 + parseInt( sunrise[5] ), - parseInt( sunset[4] ) * 60 + parseInt( sunset[5] ) - ]; -} - -// Checks if the weather data meets any of the restrictions set by OpenSprinkler. -// Restrictions prevent any watering from occurring and are similar to 0% watering level. -// California watering restriction prevents watering if precipitation over two days is greater -// than 0.01" over the past 48 hours. -function checkWeatherRestriction( adjustmentValue, weather ) { - var californiaRestriction = ( adjustmentValue >> 7 ) & 1; - - if ( californiaRestriction ) { - - // If the California watering restriction is in use then prevent watering - // if more then 0.01" of rain has accumulated in the past 48 hours - if ( weather.observation.imperial.precip_2day > 0.01 || weather.observation.imperial.precip_24hour > 0.01 ) { - return true; - } - } - - return false; -} - -// API Handler when using the weatherX.py where X represents the -// adjustment method which is encoded to also carry the watering -// restriction and therefore must be decoded -exports.getWeather = function( req, res ) { - var adjustmentMethod = req.params[0] & ~( 1 << 7 ), - adjustmentOptions = req.query.wto, - location = req.query.loc, - weatherUndergroundKey = req.query.key, - outputFormat = req.query.format, - firmwareVersion = req.query.fwv, - remoteAddress = req.headers[ "x-forwarded-for" ] || req.connection.remoteAddress, - finishRequest = function( weather ) { - if ( !weather || typeof weather.observation !== "object" || typeof weather.observation.imperial !== "object" ) { - res.send( "Error: No weather data found." ); - return; - } - - var data = { - scale: calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ), - restrict: checkWeatherRestriction( req.params[0], weather ) ? 1 : 0, - tz: getTimezone( weather.observation.obs_time_local ), - sunrise: getSunData( weather )[0], - sunset: getSunData( weather )[1], - eip: ipToInt( remoteAddress ) - }; - - // Return the response to the client - if ( outputFormat === "json" ) { - res.json( data ); - } else { - res.send( "&scale=" + data.scale + - "&restrict=" + data.restrict + - "&tz=" + data.tz + - "&sunrise=" + data.sunrise + - "&sunset=" + data.sunset + - "&eip=" + data.eip - ); - } + // Define regex filters to match against location + var http = require( "http" ), + filters = { + gps: /^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/, + pws: /^(?:pws|icao):/, + url: /^https?:\/\/([\w\.-]+)(:\d+)?(\/.*)?$/, + time: /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})([+-])(\d{2})(\d{2})/ }; - // Exit if no location is provided - if ( !location ) { - res.send( "Error: No location provided." ); - return; + // Generic HTTP request handler that parses the URL and uses the + // native Node.js http module to perform the request + function httpRequest( url, callback ) { + url = url.match( filters.url ); + + var options = { + host: url[1], + port: url[2] || 80, + path: url[3] + }; + + http.get( options, function( response ) { + var data = ""; + + // Reassemble the data as it comes in + response.on( "data", function( chunk ) { + data += chunk; + } ); + + // Once the data is completely received, return it to the callback + response.on( "end", function() { + callback( data ); + } ); + } ).on( "error", function() { + + // If the HTTP request fails, return false + callback( false ); + } ); } - remoteAddress = remoteAddress.split(",")[0]; - - // Parse weather adjustment options - try { - - // Reconstruct JSON string from deformed controller output - adjustmentOptions = JSON.parse( "{" + adjustmentOptions + "}" ); - } catch (err) { - adjustmentOptions = false; + // Converts IP string to integer + function ipToInt( ip ) { + ip = ip.split( "." ); + return ( ( ( ( ( ( +ip[0] ) * 256 ) + ( +ip[1] ) ) * 256 ) + ( +ip[2] ) ) * 256 ) + ( +ip[3] ); } - // Parse location string - if ( filters.gps.test( location ) ) { + // Takes a PWS or ICAO location and resolves the GPS coordinates + function getPWSCoordinates( location, weatherUndergroundKey, callback ) { + var url = "http://api.wunderground.com/api/" + weatherUndergroundKey + + "/conditions/forecast/q/" + encodeURIComponent( location ) + ".json"; - // Handle GPS coordinates - location = location.split( "," ); - location = [ parseFloat( location[0] ), parseFloat( location[1] ) ]; - getWeatherData( location, finishRequest ); + httpRequest( url, function( data ) { + data = JSON.parse( data ); - } else if ( filters.pws.test( location ) ) { + if ( typeof data === "object" && data.current_observation && data.current_observation.observation_location ) { + callback( [ data.current_observation.observation_location.latitude, + data.current_observation.observation_location.longitude ] ); + } else { + callback( false ); + } + } ); + } - if ( !weatherUndergroundKey ) { - res.send( "Error: Weather Underground key required when using PWS or ICAO location." ); + // If location does not match GPS or PWS/ICAO, then attempt to resolve + // location using Weather Underground autocomplete API + function resolveCoordinates( location, callback ) { + var url = "http://autocomplete.wunderground.com/aq?h=0&query=" + + encodeURIComponent( location ); + + httpRequest( url, function( data ) { + data = JSON.parse( data ); + if ( typeof data.RESULTS === "object" && data.RESULTS.length ) { + callback( [ data.RESULTS[0].lat, data.RESULTS[0].lon ] ); + } + } ); + } + + // Accepts a time string formatted in ISO-8601 and returns the timezone. + // The timezone output is formatted for OpenSprinkler Unified firmware. + function getTimezone( time ) { + time = time.match( filters.time ); + + var hour = parseInt( time[7] + time[8] ), + minute = parseInt( time[9] ); + + minute = ( minute / 15 >> 0 ) / 4.0; + hour = hour + ( hour >=0 ? minute : -minute ); + + return ( ( hour + 12 ) * 4 ) >> 0; + } + + // Retrieve weather data to complete the weather request + function getWeatherData( location, callback ) { + + // Get the API key from the environment variables + var WSI_API_KEY = process.env.WSI_API_KEY, + + // Generate URL using The Weather Company API v1 in Imperial units + url = "http://api.weather.com/v1/geocode/" + location[0] + "/" + location[1] + + "/observations/current.json?apiKey=" + WSI_API_KEY + "&language=en-US&units=e"; + + // Perform the HTTP request to retrieve the weather data + httpRequest( url, function( data ) { + callback( JSON.parse( data ) ); + } ); + } + + // Calculates the resulting water scale using the provided weather data, adjustment method and options + function calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ) { + + // Calculate the average temperature + var temp = ( weather.observation.imperial.temp_max_24hour + weather.observation.imperial.temp_min_24hour ) / 2, + + // Relative humidity and if unavailable default to 0 + rh = weather.observation.imperial.rh || 0, + + // The absolute precipitation in the past 48 hours + precip = weather.observation.imperial.precip_2day || weather.observation.imperial.precip_24hour; + + if ( typeof temp !== "number" ) { + + // If the maximum and minimum temperatures are not available then use the current temperature + temp = weather.observation.imperial.temp; + } + + // Zimmerman method + if ( adjustmentMethod == 1 ) { + + var humidityFactor = ( 30 - rh ), + tempFactor = ( ( temp - 70 ) * 4 ), + precipFactor = ( precip * -2 ); + + // Apply adjustment options if available + if ( adjustmentOptions ) { + if ( adjustmentOptions.hasOwnProperty( "h" ) ) { + humidityFactor = humidityFactor * ( adjustmentOptions.h / 100 ); + } + + if ( adjustmentOptions.hasOwnProperty( "t" ) ) { + tempFactor = tempFactor * ( adjustmentOptions.t / 100 ); + } + + if ( adjustmentOptions.hasOwnProperty( "r" ) ) { + precipFactor = precipFactor * ( adjustmentOptions.r / 100 ); + } + } + + return parseInt( Math.min( Math.max( 0, 100 + humidityFactor + tempFactor + precipFactor ), 200 ) ); + } + + return -1; + } + + // Function to return the sunrise and sunset times from the weather reply + function getSunData( weather ) { + + // Sun times must be converted from strings into date objects and processed into minutes from midnight + var sunrise = weather.observation.sunrise.match( filters.time ), + sunset = weather.observation.sunset.match( filters.time ); + + return [ + parseInt( sunrise[4] ) * 60 + parseInt( sunrise[5] ), + parseInt( sunset[4] ) * 60 + parseInt( sunset[5] ) + ]; + } + + // Checks if the weather data meets any of the restrictions set by OpenSprinkler. + // Restrictions prevent any watering from occurring and are similar to 0% watering level. + // California watering restriction prevents watering if precipitation over two days is greater + // than 0.01" over the past 48 hours. + function checkWeatherRestriction( adjustmentValue, weather ) { + var californiaRestriction = ( adjustmentValue >> 7 ) & 1; + + if ( californiaRestriction ) { + + // If the California watering restriction is in use then prevent watering + // if more then 0.01" of rain has accumulated in the past 48 hours + if ( weather.observation.imperial.precip_2day > 0.01 || weather.observation.imperial.precip_24hour > 0.01 ) { + return true; + } + } + + return false; + } + + // API Handler when using the weatherX.py where X represents the + // adjustment method which is encoded to also carry the watering + // restriction and therefore must be decoded + exports.getWeather = function( req, res ) { + + // The adjustment method is encoded by the OpenSprinkler firmware and must be + // parsed. This allows the adjustment method and the restriction type to both + // be saved in the same byte. + var adjustmentMethod = req.params[0] & ~( 1 << 7 ), + adjustmentOptions = req.query.wto, + location = req.query.loc, + weatherUndergroundKey = req.query.key, + outputFormat = req.query.format, + firmwareVersion = req.query.fwv, + remoteAddress = req.headers[ "x-forwarded-for" ] || req.connection.remoteAddress, + finishRequest = function( weather ) { + if ( !weather || typeof weather.observation !== "object" || typeof weather.observation.imperial !== "object" ) { + res.send( "Error: No weather data found." ); + return; + } + + var data = { + scale: calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ), + restrict: checkWeatherRestriction( req.params[0], weather ) ? 1 : 0, + tz: getTimezone( weather.observation.obs_time_local ), + sunrise: getSunData( weather )[0], + sunset: getSunData( weather )[1], + eip: ipToInt( remoteAddress ) + }; + + // Return the response to the client + if ( outputFormat === "json" ) { + res.json( data ); + } else { + res.send( "&scale=" + data.scale + + "&restrict=" + data.restrict + + "&tz=" + data.tz + + "&sunrise=" + data.sunrise + + "&sunset=" + data.sunset + + "&eip=" + data.eip + ); + } + }; + + // Exit if no location is provided + if ( !location ) { + res.send( "Error: No location provided." ); return; } - // Handle Weather Underground specific location - getPWSCoordinates( location, weatherUndergroundKey, function( result ) { - location = result; - getWeatherData( location, finishRequest ); - } ); - } else { + remoteAddress = remoteAddress.split(",")[0]; - // Attempt to resolve provided location to GPS coordinates - resolveCoordinates( location, function( result ) { - location = result; - getWeatherData( location, finishRequest ); - } ); - } -}; + // Parse weather adjustment options + try { + + // Reconstruct JSON string from deformed controller output + adjustmentOptions = JSON.parse( "{" + adjustmentOptions + "}" ); + } catch (err) { + adjustmentOptions = false; + } + + // Parse location string + if ( filters.gps.test( location ) ) { + + // Handle GPS coordinates + location = location.split( "," ); + location = [ parseFloat( location[0] ), parseFloat( location[1] ) ]; + getWeatherData( location, finishRequest ); + + } else if ( filters.pws.test( location ) ) { + + if ( !weatherUndergroundKey ) { + res.send( "Error: Weather Underground key required when using PWS or ICAO location." ); + return; + } + + // Handle Weather Underground specific location + getPWSCoordinates( location, weatherUndergroundKey, function( result ) { + location = result; + getWeatherData( location, finishRequest ); + } ); + } else { + + // Attempt to resolve provided location to GPS coordinates + resolveCoordinates( location, function( result ) { + location = result; + getWeatherData( location, finishRequest ); + } ); + } + }; +} )();