diff --git a/package-lock.json b/package-lock.json index a6120fa..489f193 100644 --- a/package-lock.json +++ b/package-lock.json @@ -397,6 +397,14 @@ "vary": "^1" } }, + "cron": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/cron/-/cron-1.7.1.tgz", + "integrity": "sha512-gmMB/pJcqUVs/NklR1sCGlNYM7TizEw+1gebz20BMc/8bTm/r7QUp3ZPSPlG8Z5XRlvb7qhjEjq/+bdIfUCL2A==", + "requires": { + "moment-timezone": "^0.5.x" + } + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", diff --git a/package.json b/package.json index c8d722c..f6f6116 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "os-weather-service", "description": "OpenSprinkler Weather Service", - "version": "1.0.1", + "version": "1.0.2", "repository": "https://github.com/OpenSprinkler/Weather-Weather", "scripts": { "test": "mocha --exit test", @@ -9,6 +9,7 @@ }, "dependencies": { "cors": "^2.8.5", + "cron": "^1.3.0", "dotenv": "^6.2.0", "express": "^4.16.4", "geo-tz": "^4.0.2", diff --git a/routes/local.js b/routes/local.js new file mode 100755 index 0000000..5bb7542 --- /dev/null +++ b/routes/local.js @@ -0,0 +1,70 @@ +var CronJob = require( "cron" ).CronJob, + server = require( "../server.js" ), + today = {}, yesterday = {}, + count = { temp: 0, humidity: 0 }, + current_date = new Date(), + last_bucket; + +function sameDay(d1, d2) { + return d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate(); +} + +exports.captureWUStream = function( req, res ) { + var prev, curr; + + if ( !( "dateutc" in req.query ) || !sameDay( current_date, new Date( req.query.dateutc + "Z") )) { + res.send( "Error: Bad date range\n" ); + return; + } + + if ( ( "tempf" in req.query ) && !isNaN( curr = parseFloat( req.query.tempf ) ) && curr !== -9999.0 ) { + prev = ( "temp" in today ) ? today.temp : 0; + today.temp = ( prev * count.temp + curr ) / ( ++count.temp ); + } + if ( ( "humidity" in req.query ) && !isNaN( curr = parseFloat( req.query.humidity ) ) && curr !== -9999.0 ) { + prev = ( "humidity" in today ) ? today.humidity : 0; + today.humidity = ( prev * count.humidity + curr ) / ( ++count.humidity ); + } + if ( ( "dailyrainin" in req.query ) && !isNaN( curr = parseFloat( req.query.dailyrainin ) ) && curr !== -9999.0 ) { + today.precip = curr; + } + if ( ( "rainin" in req.query ) && !isNaN( curr = parseFloat( req.query.rainin ) ) && curr > 0 ) { + last_bucket = new Date(); + } + + console.log( "OpenSprinkler Weather Observation: %s", JSON.stringify( req.query ) ); + + res.send( "success\n" ); +}; + +exports.useLocalWeather = function() { + return server.pws !== "none" ? true : false; +}; + +exports.getLocalWeather = function() { + var result = {}; + + // Use today's weather if we dont have information for yesterday yet (i.e. on startup) + Object.assign( result, today, yesterday); + + if ( "precip" in yesterday && "precip" in today ) { + result.precip = yesterday.precip + today.precip; + } + + // PWS report "buckets" so consider it still raining if last bucket was less than an hour ago + if ( last_bucket !== undefined ) { + result.raining = ( ( Date.now() - last_bucket ) / 1000 / 60 / 60 < 1 ); + } + + return result; +}; + +new CronJob( "0 0 0 * * *", function() { + + yesterday = Object.assign( {}, today ); + today = Object.assign( {} ); + count.temp = 0; count.humidity = 0; + current_date = new Date(); +}, null, true ); diff --git a/routes/weather.js b/routes/weather.js index be6aeee..59d2396 100755 --- a/routes/weather.js +++ b/routes/weather.js @@ -1,4 +1,5 @@ var http = require( "http" ), + local = require( "../routes/local.js" ), SunCalc = require( "suncalc" ), moment = require( "moment-timezone" ), geoTZ = require( "geo-tz" ), @@ -16,40 +17,25 @@ var http = require( "http" ), // 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 ); + if ( filters.pws.test( location ) ) { + callback( false ); + } else if ( filters.gps.test( location ) ) { + location = location.split( "," ); + location = [ parseFloat( location[ 0 ] ), parseFloat( location[ 1 ] ) ]; + callback( location ); + } else { + // 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 && data.RESULTS[ 0 ].tz !== "MISSING" ) { - - // If it is, reply with an array containing the GPS coordinates - callback( [ data.RESULTS[ 0 ].lat, data.RESULTS[ 0 ].lon ], moment().tz( data.RESULTS[ 0 ].tz ).utcOffset() ); - } else { - - // Otherwise, indicate no data was found - callback( false ); - } - } ); -} - -function getData( url, callback ) { - - httpRequest( url, function( data ) { - try { - data = JSON.parse( data ); - } catch (err) { - callback( {} ); - return; - } - callback( data ); - return; - } ); + httpJSONRequest( url, function( data ) { + if ( typeof data.RESULTS === "object" && data.RESULTS.length ) { + callback( [ data.RESULTS[ 0 ].lat, data.RESULTS[ 0 ].lon ] ); + } else { + callback( false ); + } + } ); + } } // Retrieve data from Open Weather Map for water level calculations @@ -60,7 +46,7 @@ function getOWMWateringData( location, callback ) { getTimeData( location, function( weather ) { // Perform the HTTP request to retrieve the weather data - getData( forecastUrl, function( forecast ) { + httpJSONRequest( forecastUrl, function( forecast ) { if ( !forecast || !forecast.list ) { callback( weather ); @@ -96,9 +82,9 @@ function getOWMWeatherData( location, callback ) { getTimeData( location, function( weather ) { - getData( currentUrl, function( current ) { + httpJSONRequest( currentUrl, function( current ) { - getData( forecastDailyUrl, function( forecast ) { + httpJSONRequest( forecastDailyUrl, function( forecast ) { if ( !current || !current.main || !current.wind || !current.weather || !forecast || !forecast.list ) { callback( weather ); @@ -134,6 +120,17 @@ function getOWMWeatherData( location, callback ) { } ); } +// Retrieve weather data from Local record +function getLocalWateringData( location, callback ) { + + getTimeData( location, function( weather ) { + Object.assign( weather, local.getLocalWeather() ); + location = location.join( "," ); + + callback( weather ); + } ); +} + // Calculate timezone and sun rise/set information function getTimeData( location, callback ) { var timezone = moment().tz( geoTZ( location[ 0 ], location[ 1 ] ) ).utcOffset(); @@ -171,9 +168,8 @@ function calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ) { precipBase = adjustmentOptions.hasOwnProperty( "br" ) ? adjustmentOptions.br : precipBase; } - var temp = ( ( weather.maxTemp + weather.minTemp ) / 2 ) || weather.temp, - humidityFactor = ( humidityBase - weather.humidity ), - tempFactor = ( ( temp - tempBase ) * 4 ), + var humidityFactor = ( humidityBase - weather.humidity ), + tempFactor = ( ( weather.temp - tempBase ) * 4 ), precipFactor = ( ( precipBase - weather.precip ) * 200 ); // Apply adjustment options, if provided, by multiplying the percentage against the factor @@ -222,34 +218,20 @@ function checkWeatherRestriction( adjustmentValue, weather ) { exports.getWeatherData = function( req, res ) { var location = req.query.loc; - if ( filters.gps.test( location ) ) { + // 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 ) { + if ( result === false ) { + res.send( "Error: Unable to resolve location" ); + return; + } - // 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 + location = result; getOWMWeatherData( location, function( data ) { data.location = location; res.json( data ); } ); - } else { - - // 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 ) { - if ( result === false ) { - res.send( "Error: Unable to resolve location" ); - return; - } - - location = result; - getOWMWeatherData( location, function( data ) { - data.location = location; - res.json( data ); - } ); - } ); - } + } ); }; // API Handler when using the weatherX.py where X represents the @@ -310,13 +292,17 @@ exports.getWateringData = function( req, res ) { sunset: weather.sunset, eip: ipToInt( remoteAddress ), rawData: { - h: weather.humidity, + h: Math.round( weather.humidity ), p: Math.round( weather.precip * 100 ) / 100, t: Math.round( weather.temp * 10 ) / 10, raining: weather.raining ? 1 : 0 } }; + if ( local.useLocalWeather() ) { + console.log( "OpenSprinkler Weather Response: %s", JSON.stringify( data ) ); + } + // Return the response to the client in the requested format if ( outputFormat === "json" ) { res.json( data ); @@ -332,6 +318,10 @@ exports.getWateringData = function( req, res ) { } }; + if ( local.useLocalWeather() ) { + console.log( "OpenSprinkler Weather Query: %s", JSON.stringify( req.query ) ); + } + // Exit if no location is provided if ( !location ) { res.send( "Error: No location provided." ); @@ -356,39 +346,26 @@ exports.getWateringData = function( req, res ) { adjustmentOptions = false; } - // Parse location string - if ( filters.pws.test( location ) ) { + // 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 ) { + if ( result === false ) { + res.send( "Error: Unable to resolve location" ); + return; + } - // Weather Underground is discontinued and PWS or ICAO cannot be resolved - res.send( "Error: Weather Underground is discontinued." ); - return; - } 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 - getOWMWateringData( location, finishRequest ); - } else { - - // 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 ) { - if ( result === false ) { - res.send( "Error: Unable to resolve location" ); - return; - } - - location = result; + location = result; + if ( local.useLocalWeather() ) { + getLocalWateringData( location, finishRequest ); + } else { getOWMWateringData( location, finishRequest ); - } ); - } + } + } ); }; // Generic HTTP request handler that parses the URL and uses the -// native Node.js http module to perform the request -function httpRequest( url, callback ) { +// native Node.js http module to perform the request. Returns a Json object +function httpJSONRequest( url, callback ) { url = url.match( filters.url ); var options = { @@ -407,6 +384,7 @@ function httpRequest( url, callback ) { // Once the data is completely received, return it to the callback response.on( "end", function() { + data = JSON.parse( data ); callback( data ); } ); } ).on( "error", function() { diff --git a/server.js b/server.js index 46b3c7a..4383799 100755 --- a/server.js +++ b/server.js @@ -1,14 +1,18 @@ -var express = require( "express" ), +var package = require( "./package.json" ), + express = require( "express" ), weather = require( "./routes/weather.js" ), + local = require( "./routes/local.js" ), cors = require( "cors" ), host = process.env.HOST || "127.0.0.1", port = process.env.PORT || 3000, + pws = process.env.PWS || "none", app = express(); -if ( !process.env.HOST || !process.env.PORT ) { +if ( !process.env.HOST || !process.env.PORT || !process.env.LOCAL_PWS ) { require( "dotenv" ).load(); host = process.env.HOST || host; port = process.env.PORT || port; + pws = process.env.PWS || pws; } // Handle requests matching /weatherID.py where ID corresponds to the @@ -21,8 +25,13 @@ app.get( /(\d+)/, weather.getWateringData ); app.options( /weatherData/, cors() ); app.get( /weatherData/, cors(), weather.getWeatherData ); +// Endpoint to stream Weather Underground data from local PWS +if ( pws === "WU" ) { + app.get( "/weatherstation/updateweatherstation.php", local.captureWUStream ); +} + app.get( "/", function( req, res ) { - res.send( "OpenSprinkler Weather Service" ); + res.send( package.description + " v" + package.version ); } ); // Handle 404 error @@ -33,7 +42,12 @@ app.use( function( req, res ) { // Start listening on the service port app.listen( port, host, function() { - console.log( "OpenSprinkler Weather Service now listening on %s:%s", host, port ); + console.log( "%s now listening on %s:%s", package.description, host, port ); + + if (pws !== "none" ) { + console.log( "%s now listening for local weather stream", package.description ); + } } ); exports.app = app; +exports.pws = pws;