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..39c479e 100644 --- a/package.json +++ b/package.json @@ -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..45d92a9 --- /dev/null +++ b/routes/local.js @@ -0,0 +1,62 @@ +var CronJob = require( "cron" ).CronJob; +var server = require( "../server.js" ); +var today = {}, yesterday = {}; +var count = { temp: 0, humidity: 0 }; +var current_date = new Date(); +var last_rain = new Date().setTime(0); + +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_rain = new Date(); + } + + res.send( "success\n" ); +}; + +exports.hasLocalWeather = function() { + return ( server.pws !== "false" ? 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); + Object.assign( result, ( yesterday.precip && today.precip ) ? { precip: yesterday.precip + today.precip } : {} ); + + result.raining = ( ( Date.now() - last_rain ) / 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..4b58e64 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" ), @@ -24,10 +25,10 @@ function resolveCoordinates( location, callback ) { // 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 { @@ -134,6 +135,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(); @@ -234,7 +246,6 @@ exports.getWeatherData = function( req, res ) { 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 ) { @@ -249,7 +260,7 @@ exports.getWeatherData = function( req, res ) { res.json( data ); } ); } ); - } + } }; // API Handler when using the weatherX.py where X represents the @@ -310,13 +321,15 @@ 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 } }; + 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 +345,8 @@ exports.getWateringData = function( req, res ) { } }; + console.log( "OpenSprinkler Weather Query: %s", JSON.stringify( req.query ) ); + // Exit if no location is provided if ( !location ) { res.send( "Error: No location provided." ); @@ -371,7 +386,6 @@ exports.getWateringData = function( req, res ) { // 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 ) { @@ -383,7 +397,7 @@ exports.getWateringData = function( req, res ) { location = result; getOWMWateringData( location, finishRequest ); } ); - } + } }; // Generic HTTP request handler that parses the URL and uses the diff --git a/server.js b/server.js index 46b3c7a..680f345 100755 --- a/server.js +++ b/server.js @@ -1,14 +1,17 @@ var 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,6 +24,11 @@ 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" ); } ); @@ -34,6 +42,11 @@ 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 ); + + if (pws !== "none" ) { + console.log( "OpenSprinkler Weather Service now listening for local weather stream" ); + } } ); exports.app = app; +exports.pws = pws;