Add the ability to retrieve weather data from Weather Underground

This commit is contained in:
Samer Albahra
2015-07-03 02:53:51 -05:00
parent cd67a6f997
commit 288733a190
2 changed files with 98 additions and 52 deletions

View File

@@ -10,6 +10,7 @@
"grunt": "^0.4.5", "grunt": "^0.4.5",
"grunt-contrib-jshint": "^0.11.2", "grunt-contrib-jshint": "^0.11.2",
"mongoose": "^4.0.6", "mongoose": "^4.0.6",
"suncalc": "^1.6.0",
"xml2js": "^0.4.9" "xml2js": "^0.4.9"
} }
} }

View File

@@ -1,4 +1,5 @@
var http = require( "http" ), var http = require( "http" ),
SunCalc = require( "suncalc" ),
// parseXML = require( "xml2js" ).parseString, // parseXML = require( "xml2js" ).parseString,
Cache = require( "../models/Cache" ), Cache = require( "../models/Cache" ),
@@ -7,7 +8,8 @@ var http = require( "http" ),
gps: /^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/, gps: /^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/,
pws: /^(?:pws|icao):/, pws: /^(?:pws|icao):/,
url: /^https?:\/\/([\w\.-]+)(:\d+)?(\/.*)?$/, url: /^https?:\/\/([\w\.-]+)(:\d+)?(\/.*)?$/,
time: /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})([+-])(\d{2})(\d{2})/ time: /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})([+-])(\d{2})(\d{2})/,
timezone: /^()()()()()()([+-])(\d{2})(\d{2})/
}; };
// Takes a PWS or ICAO location and resolves the GPS coordinates // Takes a PWS or ICAO location and resolves the GPS coordinates
@@ -53,7 +55,47 @@ function resolveCoordinates( location, callback ) {
} ); } );
} }
// Retrieve weather data to complete the weather request // Retrieve weather data to complete the weather request using Weather Underground
function getWeatherUndergroundData( location, weatherUndergroundKey, callback ) {
// Generate URL using The Weather Company API v1 in Imperial units
var url = "http://api.wunderground.com/api/" + weatherUndergroundKey +
"/yesterday/conditions/q/" + location + ".json";
// Perform the HTTP request to retrieve the weather data
httpRequest( url, function( data ) {
try {
var data = JSON.parse( data );
// Calculate sunrise and sunset since Weather Underground does not provide it
var sunData = SunCalc.getTimes( new Date(), data.current_observation.observation_location.latitude, data.current_observation.observation_location.longitude ),
weather = {
icon: data.current_observation.icon,
timezone: data.current_observation.local_tz_offset,
sunrise: ( sunData.sunrise.getHours() * 60 + sunData.sunrise.getMinutes() ),
sunset: ( sunData.sunset.getHours() * 60 + sunData.sunset.getMinutes() ),
maxTemp: parseInt( data.history.dailysummary[0].maxtempi ),
minTemp: parseInt( data.history.dailysummary[0].mintempi ),
temp: data.current_observation.temp_f,
humidity: ( parseInt( data.history.dailysummary[0].maxhumidity ) + parseInt( data.history.dailysummary[0].minhumidity ) ) / 2,
precip: parseInt( data.current_observation.precip_today_in ) + parseInt( data.history.dailysummary[0].precipi ),
solar: parseInt( data.current_observation.UV ),
wind: parseInt( data.history.dailysummary[0].meanwindspdi ),
elevation: data.current_observation.observation_location.elevation
};
callback( weather );
} catch ( err ) {
// Otherwise indicate the request failed
callback( false );
}
} );
}
// Retrieve weather data to complete the weather request using The Weather Channel
function getWeatherData( location, callback ) { function getWeatherData( location, callback ) {
// Get the API key from the environment variables // Get the API key from the environment variables
@@ -67,7 +109,20 @@ function getWeatherData( location, callback ) {
httpRequest( url, function( data ) { httpRequest( url, function( data ) {
try { try {
var weather = JSON.parse( data ); var data = JSON.parse( data ),
weather = {
iconCode: data.observation.icon_code,
timezone: data.observation.obs_time_local,
sunrise: parseDayTime( data.observation.sunrise ),
sunset: parseDayTime( data.observation.sunset ),
maxTemp: data.observation.imperial.temp_max_24hour,
minTemp: data.observation.imperial.temp_min_24hour,
temp: data.observation.imperial.temp,
humidity: data.observation.imperial.rh || 0,
precip: data.observation.imperial.precip_2day || data.observation.imperial.precip_24hour,
solar: data.observation.imperial.uv_index,
wind: data.observation.imperial.wspd
};
location = location.join( "," ); location = location.join( "," );
@@ -122,7 +177,7 @@ function updateCache( location, weather ) {
// If a record is found update the data and save it // If a record is found update the data and save it
if ( record ) { if ( record ) {
record.currentHumidityTotal += weather.observation.imperial.rh; record.currentHumidityTotal += weather.humidity;
record.currentHumidityCount++; record.currentHumidityCount++;
record.save(); record.save();
@@ -131,7 +186,7 @@ function updateCache( location, weather ) {
// If no cache record is found, generate a new one and save it // If no cache record is found, generate a new one and save it
new Cache( { new Cache( {
location: location, location: location,
currentHumidityTotal: weather.observation.imperial.rh, currentHumidityTotal: weather.humidity,
currentHumidityCount: 1 currentHumidityCount: 1
} ).save(); } ).save();
@@ -142,28 +197,14 @@ function updateCache( location, weather ) {
// Calculates the resulting water scale using the provided weather data, adjustment method and options // Calculates the resulting water scale using the provided weather data, adjustment method and options
function calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ) { 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.yesterdayHumidity || 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 // Zimmerman method
if ( adjustmentMethod === 1 ) { if ( adjustmentMethod === 1 ) {
var humidityFactor = ( 30 - rh ), var temp = ( ( weather.maxTemp + weather.minTemp ) / 2 ) || weather.temp,
humidityFactor = ( 30 - weather.humidity ),
tempFactor = ( ( temp - 70 ) * 4 ), tempFactor = ( ( temp - 70 ) * 4 ),
precipFactor = ( precip * -2 ); precipFactor = ( weather.precip * -200 );
console.log(temp, humidityFactor, tempFactor, precipFactor);
// Apply adjustment options, if provided, by multiplying the percentage against the factor // Apply adjustment options, if provided, by multiplying the percentage against the factor
if ( adjustmentOptions ) { if ( adjustmentOptions ) {
if ( adjustmentOptions.hasOwnProperty( "h" ) ) { if ( adjustmentOptions.hasOwnProperty( "h" ) ) {
@@ -196,9 +237,10 @@ function calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ) {
function checkWeatherRestriction( adjustmentValue, weather ) { function checkWeatherRestriction( adjustmentValue, weather ) {
// Define all the weather codes that indicate rain // 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 ]; 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 ],
adverseWords = [ "flurries", "sleet", "rain", "sleet", "snow", "tstorms" ];
if ( adverseCodes.indexOf( weather.observation.icon_code ) !== -1 ) { if ( ( weather.iconCode && adverseCodes.indexOf( weather.iconCode ) !== -1 ) || ( weather.icon && adverseWords.indexOf( weather.icon ) !== -1 ) ) {
// If the current weather indicates rain, add a restrict flag to the weather script indicating // If the current weather indicates rain, add a restrict flag to the weather script indicating
// the controller should not water. // the controller should not water.
@@ -211,7 +253,7 @@ function checkWeatherRestriction( adjustmentValue, weather ) {
// If the California watering restriction is in use then prevent watering // 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 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 ) { if ( weather.precip > 0.01 ) {
return true; return true;
} }
} }
@@ -239,7 +281,7 @@ exports.getWeather = function( req, res ) {
// Data will be processed to retrieve the resulting scale, sunrise/sunset, timezone, // Data will be processed to retrieve the resulting scale, sunrise/sunset, timezone,
// and also calculate if a restriction is met to prevent watering. // and also calculate if a restriction is met to prevent watering.
finishRequest = function( weather ) { finishRequest = function( weather ) {
if ( !weather || typeof weather.observation !== "object" || typeof weather.observation.imperial !== "object" ) { if ( !weather ) {
res.send( "Error: No weather data found." ); res.send( "Error: No weather data found." );
return; return;
} }
@@ -247,9 +289,9 @@ exports.getWeather = function( req, res ) {
var data = { var data = {
scale: calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ), scale: calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ),
restrict: checkWeatherRestriction( req.params[0], weather ) ? 1 : 0, restrict: checkWeatherRestriction( req.params[0], weather ) ? 1 : 0,
tz: getTimezone( weather.observation.obs_time_local ), tz: getTimezone( weather.timezone ),
sunrise: getSunData( weather )[0], sunrise: weather.sunrise,
sunset: getSunData( weather )[1], sunset: weather.sunset,
eip: ipToInt( remoteAddress ) eip: ipToInt( remoteAddress )
}; };
@@ -289,16 +331,7 @@ exports.getWeather = function( req, res ) {
} }
// Parse location string // Parse location string
if ( filters.gps.test( location ) ) { if ( filters.pws.test( location ) ) {
// 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) // Handle locations using PWS or ICAO (Weather Underground)
if ( !weatherUndergroundKey ) { if ( !weatherUndergroundKey ) {
@@ -317,6 +350,22 @@ exports.getWeather = function( req, res ) {
location = result; location = result;
getWeatherData( location, finishRequest ); getWeatherData( location, finishRequest );
} ); } );
} else if ( weatherUndergroundKey ) {
// The current weather script uses Weather Underground and during the transition period
// both will be supported and users who provide a Weather Underground API key will continue
// using Weather Underground until The Weather Service becomes the default API
getWeatherUndergroundData( location, weatherUndergroundKey, finishRequest );
} else if ( filters.gps.test( location ) ) {
// 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 { } else {
// Attempt to resolve provided location to GPS coordinates when it does not match // Attempt to resolve provided location to GPS coordinates when it does not match
@@ -363,12 +412,13 @@ function httpRequest( url, callback ) {
} ); } );
} }
// Accepts a time string formatted in ISO-8601 and returns the timezone. // Accepts a time string formatted in ISO-8601 or just the timezone
// offset and returns the timezone.
// The timezone output is formatted for OpenSprinkler Unified firmware. // The timezone output is formatted for OpenSprinkler Unified firmware.
function getTimezone( time ) { function getTimezone( time ) {
// Match the provided time string against a regex for parsing // Match the provided time string against a regex for parsing
time = time.match( filters.time ); time = time.match( filters.time ) || time.match( filters.timezone );
var hour = parseInt( time[7] + time[8] ), var hour = parseInt( time[7] + time[8] ),
minute = parseInt( time[9] ); minute = parseInt( time[9] );
@@ -381,18 +431,13 @@ function getTimezone( time ) {
} }
// Function to return the sunrise and sunset times from the weather reply // Function to return the sunrise and sunset times from the weather reply
function getSunData( weather ) { function parseDayTime( time ) {
// Sun times are parsed from string against a regex to identify the timezone // Time is parsed from string against a regex
var sunrise = weather.observation.sunrise.match( filters.time ), time = time.match( filters.time );
sunset = weather.observation.sunset.match( filters.time );
return [
// Values are converted to minutes from midnight for the controller // Values are converted to minutes from midnight for the controller
parseInt( sunrise[4] ) * 60 + parseInt( sunrise[5] ), return parseInt( time[4] ) * 60 + parseInt( time[5] );
parseInt( sunset[4] ) * 60 + parseInt( sunset[5] )
];
} }
// Converts IP string to integer // Converts IP string to integer