diff --git a/routes/weather.js b/routes/weather.js index c70dfc9..519ad48 100644 --- a/routes/weather.js +++ b/routes/weather.js @@ -1,9 +1,10 @@ ( function() { - // Define regex filters to match against location var http = require( "http" ), parseXML = require( "xml2js" ).parseString, - filters = { + + // Define regex filters to match against location + 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+)?(\/.*)?$/, @@ -71,12 +72,20 @@ // If location does not match GPS or PWS/ICAO, then attempt to resolve // location using Weather Underground autocomplete API function resolveCoordinates( location, callback ) { + + // Generate URL for autocomplete request var url = "http://autocomplete.wunderground.com/aq?h=0&query=" + encodeURIComponent( location ); httpRequest( url, function( data ) { + + // Parse the reply for JSON data data = JSON.parse( data ); + + // Check if the data is valid if ( typeof data.RESULTS === "object" && data.RESULTS.length ) { + + // If it is, reply with an array containing the GPS coordinates callback( [ data.RESULTS[0].lat, data.RESULTS[0].lon ] ); } } ); @@ -85,12 +94,15 @@ // Accepts a time string formatted in ISO-8601 and returns the timezone. // The timezone output is formatted for OpenSprinkler Unified firmware. function getTimezone( time ) { + + // Match the provided time string against a regex for parsing time = time.match( filters.time ); var hour = parseInt( time[7] + time[8] ), minute = parseInt( time[9] ); - minute = ( minute / 15 >> 0 ) / 4.0; + // Convert the timezone into the OpenSprinkler encoded format + minute = ( minute / 15 >> 0 ) / 4; hour = hour + ( hour >=0 ? minute : -minute ); return ( ( hour + 12 ) * 4 ) >> 0; @@ -108,6 +120,8 @@ // Perform the HTTP request to retrieve the weather data httpRequest( url, function( data ) { + + // Return the data to the callback function callback( JSON.parse( data ) ); } ); } @@ -117,7 +131,11 @@ // Get the API key from the environment variables var WSI_HISTORY_KEY = process.env.WSI_HISTORY_KEY, + + // Generate a Date object for the current time today = new Date(), + + // Generate a Date object for the previous day by subtracting a day (in milliseconds) from today yesterday = new Date( today.getTime() - 1000 * 60 * 60 * 24 ), // Generate URL using WSI Cleaned History API in Imperial units showing daily average values @@ -158,7 +176,7 @@ tempFactor = ( ( temp - 70 ) * 4 ), precipFactor = ( precip * -2 ); - // Apply adjustment options if available + // Apply adjustment options, if provided, by multiplying the percentage against the factor if ( adjustmentOptions ) { if ( adjustmentOptions.hasOwnProperty( "h" ) ) { humidityFactor = humidityFactor * ( adjustmentOptions.h / 100 ); @@ -173,6 +191,7 @@ } } + // Apply all of the weather modifying factors and clamp the result between 0 and 200%. return parseInt( Math.min( Math.max( 0, 100 + humidityFactor + tempFactor + precipFactor ), 200 ) ); } @@ -182,11 +201,13 @@ // 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 + // Sun times are parsed from string against a regex to identify the timezone var sunrise = weather.observation.sunrise.match( filters.time ), sunset = weather.observation.sunset.match( filters.time ); return [ + + // Values are converted to minutes from midnight for the controller parseInt( sunrise[4] ) * 60 + parseInt( sunrise[5] ), parseInt( sunset[4] ) * 60 + parseInt( sunset[5] ) ]; @@ -194,9 +215,23 @@ // 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. + // + // All queries will return a restrict flag if the current weather indicates rain. + // // California watering restriction prevents watering if precipitation over two days is greater // than 0.01" over the past 48 hours. function checkWeatherRestriction( adjustmentValue, weather ) { + + // Define all the weather codes that indicate rain + var adverseCodes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 35, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]; + + if ( adverseCodes.indexOf( weather.observation.icon_code ) !== -1 ) { + + // If the current weather indicates rain, add a restrict flag to the weather script indicating + // the controller should not water. + return true; + } + var californiaRestriction = ( adjustmentValue >> 7 ) & 1; if ( californiaRestriction ) { @@ -226,6 +261,10 @@ outputFormat = req.query.format, firmwareVersion = req.query.fwv, remoteAddress = req.headers[ "x-forwarded-for" ] || req.connection.remoteAddress, + + // Function that will accept the weather after it is received from the API + // Data will be processed to retrieve the resulting scale, sunrise/sunset, timezone, + // and also calculate if a restriction is met to prevent watering. finishRequest = function( weather ) { if ( !weather || typeof weather.observation !== "object" || typeof weather.observation.imperial !== "object" ) { res.send( "Error: No weather data found." ); @@ -233,24 +272,24 @@ } 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 ) + 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 + res.send( "&scale=" + data.scale + + "&restrict=" + data.restrict + + "&tz=" + data.tz + + "&sunrise=" + data.sunrise + + "&sunset=" + data.sunset + + "&eip=" + data.eip ); } }; @@ -261,6 +300,8 @@ return; } + // X-Forwarded-For header may contain more than one IP address and therefore + // the string is split against a comma and the first value is selected remoteAddress = remoteAddress.split(",")[0]; // Parse weather adjustment options @@ -269,32 +310,39 @@ // Reconstruct JSON string from deformed controller output adjustmentOptions = JSON.parse( "{" + adjustmentOptions + "}" ); } catch (err) { + + // If the JSON is not valid, do not incorporate weather adjustment options adjustmentOptions = false; } // Parse location string if ( filters.gps.test( location ) ) { - // Handle GPS coordinates + // Handle GPS coordinates by storing each coordinate in an array location = location.split( "," ); location = [ parseFloat( location[0] ), parseFloat( location[1] ) ]; + + // Continue with the weather request getWeatherData( location, finishRequest ); } else if ( filters.pws.test( location ) ) { + // Handle locations using PWS or ICAO (Weather Underground) if ( !weatherUndergroundKey ) { + + // If no key is provided for Weather Underground then the PWS or ICAO cannot be resolved 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 + // Attempt to resolve provided location to GPS coordinates when it does not match + // a GPS coordinate or Weather Underground location using Weather Underground autocomplete resolveCoordinates( location, function( result ) { location = result; getWeatherData( location, finishRequest );