Merge pull request #11 from PeteBa/feature_local_weather
Add endpoint for WU format weather station
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
70
routes/local.js
Executable file
70
routes/local.js
Executable file
@@ -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 );
|
||||
@@ -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() {
|
||||
|
||||
22
server.js
22
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;
|
||||
|
||||
Reference in New Issue
Block a user