From ca21058977c84a5bf93c057b94697a2092a397e1 Mon Sep 17 00:00:00 2001 From: Pete ba Date: Sun, 5 May 2019 22:18:41 +0100 Subject: [PATCH 1/4] Rebase on latest master Revert refactor versioning Tidy revert of versioning Bump version --- package-lock.json | 8 ++++++ package.json | 1 + routes/local.js | 62 +++++++++++++++++++++++++++++++++++++++++++++++ routes/weather.js | 28 +++++++++++++++------ server.js | 15 +++++++++++- 5 files changed, 106 insertions(+), 8 deletions(-) create mode 100755 routes/local.js 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; From 612f911285932f018c1558027985873443ee0e77 Mon Sep 17 00:00:00 2001 From: Pete ba Date: Mon, 6 May 2019 12:14:15 +0100 Subject: [PATCH 2/4] Refactor to dedupe and add version --- routes/local.js | 2 +- routes/weather.js | 139 +++++++++++++++++----------------------------- server.js | 9 +-- 3 files changed, 58 insertions(+), 92 deletions(-) diff --git a/routes/local.js b/routes/local.js index 45d92a9..c54674f 100755 --- a/routes/local.js +++ b/routes/local.js @@ -37,7 +37,7 @@ exports.captureWUStream = function( req, res ) { res.send( "success\n" ); }; -exports.hasLocalWeather = function() { +exports.useLocalWeather = function() { return ( server.pws !== "false" ? true : false ); }; diff --git a/routes/weather.js b/routes/weather.js index 4b58e64..4ad7bff 100755 --- a/routes/weather.js +++ b/routes/weather.js @@ -17,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 @@ -61,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 ); @@ -97,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 ); @@ -234,33 +219,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 @@ -328,7 +300,9 @@ exports.getWateringData = function( req, res ) { } }; - console.log( "OpenSprinkler Weather Response: %s", JSON.stringify( data ) ); + 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" ) { @@ -345,7 +319,9 @@ exports.getWateringData = function( req, res ) { } }; - console.log( "OpenSprinkler Weather Query: %s", JSON.stringify( req.query ) ); + if ( local.useLocalWeather() ) { + console.log( "OpenSprinkler Weather Query: %s", JSON.stringify( req.query ) ); + } // Exit if no location is provided if ( !location ) { @@ -371,38 +347,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 = { @@ -421,6 +385,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 680f345..4383799 100755 --- a/server.js +++ b/server.js @@ -1,4 +1,5 @@ -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" ), @@ -30,7 +31,7 @@ if ( pws === "WU" ) { } app.get( "/", function( req, res ) { - res.send( "OpenSprinkler Weather Service" ); + res.send( package.description + " v" + package.version ); } ); // Handle 404 error @@ -41,10 +42,10 @@ 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( "OpenSprinkler Weather Service now listening for local weather stream" ); + console.log( "%s now listening for local weather stream", package.description ); } } ); From b4b34f392e5db85c66c39bc362d444537228811e Mon Sep 17 00:00:00 2001 From: Pete ba Date: Mon, 6 May 2019 11:48:19 +0100 Subject: [PATCH 3/4] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 39c479e..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", From 7775b2e61399c44e2593803ada67ed89bc12efde Mon Sep 17 00:00:00 2001 From: Pete ba Date: Mon, 6 May 2019 18:35:04 +0100 Subject: [PATCH 4/4] rmloeb feedback and review --- routes/local.js | 28 ++++++++++++++++++---------- routes/weather.js | 5 ++--- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/routes/local.js b/routes/local.js index c54674f..5bb7542 100755 --- a/routes/local.js +++ b/routes/local.js @@ -1,9 +1,9 @@ -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); +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() && @@ -31,14 +31,16 @@ exports.captureWUStream = function( req, res ) { today.precip = curr; } if ( ( "rainin" in req.query ) && !isNaN( curr = parseFloat( req.query.rainin ) ) && curr > 0 ) { - last_rain = new Date(); + last_bucket = new Date(); } + console.log( "OpenSprinkler Weather Observation: %s", JSON.stringify( req.query ) ); + res.send( "success\n" ); }; exports.useLocalWeather = function() { - return ( server.pws !== "false" ? true : false ); + return server.pws !== "none" ? true : false; }; exports.getLocalWeather = function() { @@ -46,9 +48,15 @@ exports.getLocalWeather = function() { // 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 ); + 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; }; diff --git a/routes/weather.js b/routes/weather.js index 4ad7bff..59d2396 100755 --- a/routes/weather.js +++ b/routes/weather.js @@ -168,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