From 6acb40eed16568bd9acfb4a56c36c34468eb51aa Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Thu, 9 May 2019 00:55:37 -0400 Subject: [PATCH 1/9] Begin refactoring to TypeScript --- .gitignore | 2 + package-lock.json | 107 ++++++++++++++++++++++++++++++ package.json | 12 +++- routes/{weather.js => weather.ts} | 4 +- server.js => server.ts | 0 tsconfig.json | 14 ++++ 6 files changed, 135 insertions(+), 4 deletions(-) rename routes/{weather.js => weather.ts} (98%) mode change 100755 => 100644 rename server.js => server.ts (100%) mode change 100755 => 100644 create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 554c800..00170b7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ node_modules coverage/* npm-debug.log .vscode +.idea +js # Elastic Beanstalk Files .elasticbeanstalk/* diff --git a/package-lock.json b/package-lock.json index a6120fa..dbc1fef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,107 @@ "@turf/helpers": "6.x" } }, + "@types/body-parser": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-GmK8AKu8i+s+EChK/uZ5IbrXPcPaQKWaNSGevDT/7o3gFObwSUQwqb1jMqxuo+YPvj0ckGzINI+EO7EHcmJjKg==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/dotenv": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-6.1.1.tgz", + "integrity": "sha512-ftQl3DtBvqHl9L16tpqqzA4YzCSXZfi7g8cQceTz5rOlYtk/IZbFjAv3mLOQlNIgOaylCQWQoBdDQHPgEBJPHg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.1.tgz", + "integrity": "sha512-V0clmJow23WeyblmACoxbHBu2JKlE5TiIme6Lem14FnPW9gsttyHtk6wq7njcdIWH1njAaFgR8gW09lgY98gQg==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.4.tgz", + "integrity": "sha512-x/8h6FHm14rPWnW2HP5likD/rsqJ3t/77OWx2PLxym0hXbeBWQmcPyHmwX+CtCQpjIfgrUdEoDFcLPwPZWiqzQ==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", + "dev": true + }, + "@types/moment-timezone": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/@types/moment-timezone/-/moment-timezone-0.5.12.tgz", + "integrity": "sha512-hnHH2+Efg2vExr/dSz+IX860nSiyk9Sk4pJF2EmS11lRpMcNXeB4KBW5xcgw2QPsb9amTXdsVNEe5IoJXiT0uw==", + "dev": true, + "requires": { + "moment": ">=2.14.0" + } + }, + "@types/node": { + "version": "10.14.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.6.tgz", + "integrity": "sha512-Fvm24+u85lGmV4hT5G++aht2C5I4Z4dYlWZIh62FAfFO/TfzXtPpoLI6I7AuBWkIFqZCnhFOoTT7RjjaIL5Fjg==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, + "@types/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "@types/suncalc": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@types/suncalc/-/suncalc-1.8.0.tgz", + "integrity": "sha512-1Bx7KgoCLP8LuKaY9whWiX0Y8JMEB9gmZHNJigainwFuv3gEkZvTx0AGNvnA5nSu1daQcJDKScm9tNpW/ZjpjA==", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -5755,6 +5856,12 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, + "typescript": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", + "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", + "dev": true + }, "underscore.string": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", diff --git a/package.json b/package.json index c8d722c..1b484f0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "repository": "https://github.com/OpenSprinkler/Weather-Weather", "scripts": { "test": "mocha --exit test", - "start": "node server" + "start": "node js/server", + "compile": "tsc" }, "dependencies": { "cors": "^2.8.5", @@ -17,11 +18,18 @@ "suncalc": "^1.8.0" }, "devDependencies": { + "@types/cors": "^2.8.5", + "@types/dotenv": "^6.1.1", + "@types/express": "^4.16.1", + "@types/moment-timezone": "^0.5.12", + "@types/node": "^10.14.6", + "@types/suncalc": "^1.8.0", "chai": "^4.2.0", "grunt-contrib-jshint": "^1.1.0", "hippie": "^0.5.2", "istanbul": "^0.4.5", "mocha": "^5.2.0", - "nock": "^9.6.1" + "nock": "^9.6.1", + "typescript": "^3.4.5" } } diff --git a/routes/weather.js b/routes/weather.ts old mode 100755 new mode 100644 similarity index 98% rename from routes/weather.js rename to routes/weather.ts index be6aeee..b9ec3ae --- a/routes/weather.js +++ b/routes/weather.ts @@ -192,7 +192,7 @@ function calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ) { } // Apply all of the weather modifying factors and clamp the result between 0 and 200%. - return parseInt( Math.min( Math.max( 0, 100 + humidityFactor + tempFactor + precipFactor ), 200 ) ); + return Math.floor( Math.min( Math.max( 0, 100 + humidityFactor + tempFactor + precipFactor ), 200 ) ); } return -1; @@ -305,7 +305,7 @@ exports.getWateringData = function( req, res ) { var data = { scale: scale, rd: rainDelay, - tz: getTimezone( weather.timezone ), + tz: getTimezone( weather.timezone, undefined ), sunrise: weather.sunrise, sunset: weather.sunset, eip: ipToInt( remoteAddress ), diff --git a/server.js b/server.ts old mode 100755 new mode 100644 similarity index 100% rename from server.js rename to server.ts diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f850c6d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es6", + "noImplicitReturns": true, + "noEmitOnError": true, + "outDir": "js/", + "sourceMap": true, + "skipLibCheck": true + }, + "include": [ + "server.ts", + "routes/**/*" + ] +} From f2f40d22537b71c3025364b1ae5664dcb78dd29f Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Thu, 9 May 2019 13:20:49 -0400 Subject: [PATCH 2/9] Update getData and httpRequest to modern TypeScript --- routes/weather.ts | 231 +++++++++++++++++++++++++--------------------- 1 file changed, 127 insertions(+), 104 deletions(-) diff --git a/routes/weather.ts b/routes/weather.ts index b9ec3ae..aa13fa8 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -14,42 +14,46 @@ var http = require( "http" ), // If location does not match GPS or PWS/ICAO, then attempt to resolve // location using Weather Underground autocomplete API -function resolveCoordinates( location, callback ) { +async function resolveCoordinates( location, callback ) { // Generate URL for autocomplete request var url = "http://autocomplete.wunderground.com/aq?h=0&query=" + encodeURIComponent( location ); - httpRequest( url, function( data ) { + let data; + try { + data = await getData( url ); + } catch (err) { + // If the request fails, indicate no data was found. + callback( false ); + } - // 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" ) { - // 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 { - // 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 ); - } - } ); + // 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; - } ); +/** + * Makes an HTTP GET request to the specified URL and parses the JSON response body. + * @param url The URL to fetch. + * @return A Promise that will be resolved the with parsed response body if the request succeeds, or will be rejected + * with an Error if the request or JSON parsing fails. + */ +async function getData( url: string ): Promise< any > { + try { + const data: string = await httpRequest(url); + return JSON.parse(data); + } catch (err) { + // Reject the promise if there was an error making the request or parsing the JSON. + throw err; + } } // Retrieve data from Open Weather Map for water level calculations @@ -57,34 +61,41 @@ function getOWMWateringData( location, callback ) { var OWM_API_KEY = process.env.OWM_API_KEY, forecastUrl = "http://api.openweathermap.org/data/2.5/forecast?appid=" + OWM_API_KEY + "&units=imperial&lat=" + location[ 0 ] + "&lon=" + location[ 1 ]; - getTimeData( location, function( weather ) { + getTimeData( location, async function( weather ) { // Perform the HTTP request to retrieve the weather data - getData( forecastUrl, function( forecast ) { - - if ( !forecast || !forecast.list ) { - callback( weather ); - return; - } - - weather.temp = 0; - weather.humidity = 0; - weather.precip = 0; - - var periods = Math.min(forecast.list.length, 10); - for ( var index = 0; index < periods; index++ ) { - weather.temp += parseFloat( forecast.list[ index ].main.temp ); - weather.humidity += parseInt( forecast.list[ index ].main.humidity ); - weather.precip += ( forecast.list[ index ].rain ? parseFloat( forecast.list[ index ].rain[ "3h" ] || 0 ) : 0 ); - } - - weather.temp = weather.temp / periods; - weather.humidity = weather.humidity / periods; - weather.precip = weather.precip / 25.4; - weather.raining = ( forecast.list[ 0 ].rain ? ( parseFloat( forecast.list[ 0 ].rain[ "3h" ] || 0 ) > 0 ) : false ); - + let forecast; + try { + forecast = await getData( forecastUrl ); + } catch (err) { + // Return just the time data if retrieving the forecast fails. callback( weather ); - } ); + return; + } + + // Return just the time data if the forecast data is incomplete. + if ( !forecast || !forecast.list ) { + callback( weather ); + return; + } + + weather.temp = 0; + weather.humidity = 0; + weather.precip = 0; + + var periods = Math.min(forecast.list.length, 10); + for ( var index = 0; index < periods; index++ ) { + weather.temp += parseFloat( forecast.list[ index ].main.temp ); + weather.humidity += parseInt( forecast.list[ index ].main.humidity ); + weather.precip += ( forecast.list[ index ].rain ? parseFloat( forecast.list[ index ].rain[ "3h" ] || 0 ) : 0 ); + } + + weather.temp = weather.temp / periods; + weather.humidity = weather.humidity / periods; + weather.precip = weather.precip / 25.4; + weather.raining = ( forecast.list[ 0 ].rain ? ( parseFloat( forecast.list[ 0 ].rain[ "3h" ] || 0 ) > 0 ) : false ); + + callback( weather ); } ); } @@ -94,43 +105,48 @@ function getOWMWeatherData( location, callback ) { currentUrl = "http://api.openweathermap.org/data/2.5/weather?appid=" + OWM_API_KEY + "&units=imperial&lat=" + location[ 0 ] + "&lon=" + location[ 1 ], forecastDailyUrl = "http://api.openweathermap.org/data/2.5/forecast/daily?appid=" + OWM_API_KEY + "&units=imperial&lat=" + location[ 0 ] + "&lon=" + location[ 1 ]; - getTimeData( location, function( weather ) { + getTimeData( location, async function( weather ) { - getData( currentUrl, function( current ) { + let current, forecast; + try { + current = await getData( currentUrl ); + forecast = await getData( forecastDailyUrl ); + } catch (err) { + // Return just the time data if retrieving weather data fails. + callback( weather ); + return; + } - getData( forecastDailyUrl, function( forecast ) { + // Return just the time data if the weather data is incomplete. + if ( !current || !current.main || !current.wind || !current.weather || !forecast || !forecast.list ) { + callback( weather ); + return; + } - if ( !current || !current.main || !current.wind || !current.weather || !forecast || !forecast.list ) { - callback( weather ); - return; - } + weather.temp = parseInt( current.main.temp ); + weather.humidity = parseInt( current.main.humidity ); + weather.wind = parseInt( current.wind.speed ); + weather.description = current.weather[0].description; + weather.icon = current.weather[0].icon; - weather.temp = parseInt( current.main.temp ); - weather.humidity = parseInt( current.main.humidity ); - weather.wind = parseInt( current.wind.speed ); - weather.description = current.weather[0].description; - weather.icon = current.weather[0].icon; + weather.region = forecast.city.country; + weather.city = forecast.city.name; + weather.minTemp = parseInt( forecast.list[ 0 ].temp.min ); + weather.maxTemp = parseInt( forecast.list[ 0 ].temp.max ); + weather.precip = ( forecast.list[ 0 ].rain ? parseFloat( forecast.list[ 0 ].rain || 0 ) : 0 ) / 25.4; + weather.forecast = []; - weather.region = forecast.city.country; - weather.city = forecast.city.name; - weather.minTemp = parseInt( forecast.list[ 0 ].temp.min ); - weather.maxTemp = parseInt( forecast.list[ 0 ].temp.max ); - weather.precip = ( forecast.list[ 0 ].rain ? parseFloat( forecast.list[ 0 ].rain || 0 ) : 0 ) / 25.4; - weather.forecast = []; - - for ( var index = 0; index < forecast.list.length; index++ ) { - weather.forecast.push( { - temp_min: parseInt( forecast.list[ index ].temp.min ), - temp_max: parseInt( forecast.list[ index ].temp.max ), - date: parseInt( forecast.list[ index ].dt ), - icon: forecast.list[ index ].weather[ 0 ].icon, - description: forecast.list[ index ].weather[ 0 ].description - } ); - } - - callback( weather ); + for ( var index = 0; index < forecast.list.length; index++ ) { + weather.forecast.push( { + temp_min: parseInt( forecast.list[ index ].temp.min ), + temp_max: parseInt( forecast.list[ index ].temp.max ), + date: parseInt( forecast.list[ index ].dt ), + icon: forecast.list[ index ].weather[ 0 ].icon, + description: forecast.list[ index ].weather[ 0 ].description } ); - } ); + } + + callback( weather ); } ); } @@ -386,33 +402,40 @@ exports.getWateringData = function( req, res ) { } }; -// Generic HTTP request handler that parses the URL and uses the -// native Node.js http module to perform the request -function httpRequest( url, callback ) { - url = url.match( filters.url ); +/** + * Makes an HTTP GET request to the specified URL and returns the response body. + * @param url The URL to fetch. + * @return A Promise that will be resolved the with response body if the request succeeds, or will be rejected with an + * Error if the request fails. + */ +async function httpRequest( url: string ): Promise< string > { + return new Promise< any >( ( resolve, reject ) => { - var options = { - host: url[ 1 ], - port: url[ 2 ] || 80, - path: url[ 3 ] - }; + const splitUrl: string[] = url.match( filters.url ); - http.get( options, function( response ) { - var data = ""; + const options = { + host: splitUrl[ 1 ], + port: splitUrl[ 2 ] || 80, + path: splitUrl[ 3 ] + }; - // Reassemble the data as it comes in - response.on( "data", function( chunk ) { - data += chunk; - } ); + http.get( options, ( response ) => { + let data = ""; - // Once the data is completely received, return it to the callback - response.on( "end", function() { - callback( data ); - } ); - } ).on( "error", function() { + // Reassemble the data as it comes in + response.on( "data", ( chunk ) => { + data += chunk; + } ); - // If the HTTP request fails, return false - callback( false ); + // Once the data is completely received, resolve the promise + response.on( "end", () => { + resolve( data ); + } ); + } ).on( "error", ( err ) => { + + // If the HTTP request fails, reject the promise + reject( err ); + } ); } ); } From bffede77344405039a8b8b92b5017e148b9f87b3 Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Thu, 9 May 2019 15:28:18 -0400 Subject: [PATCH 3/9] Update resolveCoordinates to modern TypeScript --- routes/weather.ts | 64 +++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/routes/weather.ts b/routes/weather.ts index aa13fa8..da5cceb 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -12,12 +12,17 @@ var http = require( "http" ), timezone: /^()()()()()()([+-])(\d{2})(\d{2})/ }; -// If location does not match GPS or PWS/ICAO, then attempt to resolve -// location using Weather Underground autocomplete API -async function resolveCoordinates( location, callback ) { +/** + * Uses the Weather Underground API to resolve a location name (ZIP code, city name, country name, etc.) to geographic + * coordinates. + * @param location A zip code or partial city/country name. + * @return A promise that will be resolved with the coordinates of the best match for the specified location, or + * rejected with an error message if unable to resolve the location. + */ +async function resolveCoordinates( location: string ): Promise< GeoCoordinates > { // Generate URL for autocomplete request - var url = "http://autocomplete.wunderground.com/aq?h=0&query=" + + const url = "http://autocomplete.wunderground.com/aq?h=0&query=" + encodeURIComponent( location ); let data; @@ -25,18 +30,18 @@ async function resolveCoordinates( location, callback ) { data = await getData( url ); } catch (err) { // If the request fails, indicate no data was found. - callback( false ); + throw "An API error occurred while attempting to resolve location"; } // 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() ); + return [ data.RESULTS[ 0 ].lat, data.RESULTS[ 0 ].lon ]; } else { // Otherwise, indicate no data was found - callback( false ); + throw "Unable to resolve location"; } } @@ -235,7 +240,7 @@ function checkWeatherRestriction( adjustmentValue, weather ) { return false; } -exports.getWeatherData = function( req, res ) { +exports.getWeatherData = async function( req, res ) { var location = req.query.loc; if ( filters.gps.test( location ) ) { @@ -253,17 +258,18 @@ exports.getWeatherData = function( req, res ) { // 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; - } + let coordinates: GeoCoordinates; + try { + coordinates = await resolveCoordinates( location ); + } catch (err) { + res.send( "Error: Unable to resolve location" ); + return; + } - location = result; - getOWMWeatherData( location, function( data ) { - data.location = location; - res.json( data ); - } ); + location = coordinates; + getOWMWeatherData( location, function( data ) { + data.location = location; + res.json( data ); } ); } }; @@ -271,7 +277,7 @@ exports.getWeatherData = function( req, res ) { // API Handler when using the weatherX.py where X represents the // adjustment method which is encoded to also carry the watering // restriction and therefore must be decoded -exports.getWateringData = function( req, res ) { +exports.getWateringData = async function( req, res ) { // The adjustment method is encoded by the OpenSprinkler firmware and must be // parsed. This allows the adjustment method and the restriction type to both @@ -390,15 +396,16 @@ exports.getWateringData = function( req, res ) { // 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; - } + let coordinates: GeoCoordinates; + try { + coordinates = await resolveCoordinates( location ); + } catch (err) { + res.send("Error: Unable to resolve location"); + return; + } - location = result; - getOWMWateringData( location, finishRequest ); - } ); + location = coordinates; + getOWMWateringData( location, finishRequest ); } }; @@ -496,3 +503,6 @@ function ipToInt( ip ) { ip = ip.split( "." ); return ( ( ( ( ( ( +ip[ 0 ] ) * 256 ) + ( +ip[ 1 ] ) ) * 256 ) + ( +ip[ 2 ] ) ) * 256 ) + ( +ip[ 3 ] ); } + +/** Geographic coordinates. The 1st element is the latitude, and the 2nd element is the longitude. */ +type GeoCoordinates = [number, number]; From cc14e089e26d89bec44689208446ba4f27b111bd Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Thu, 9 May 2019 16:19:07 -0400 Subject: [PATCH 4/9] Update getTimezone and getTimeData to modern TypeScript --- routes/weather.ts | 214 +++++++++++++++++++++++++--------------------- 1 file changed, 116 insertions(+), 98 deletions(-) diff --git a/routes/weather.ts b/routes/weather.ts index da5cceb..aa075d6 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -62,115 +62,119 @@ async function getData( url: string ): Promise< any > { } // Retrieve data from Open Weather Map for water level calculations -function getOWMWateringData( location, callback ) { +async function getOWMWateringData( location, callback ) { var OWM_API_KEY = process.env.OWM_API_KEY, forecastUrl = "http://api.openweathermap.org/data/2.5/forecast?appid=" + OWM_API_KEY + "&units=imperial&lat=" + location[ 0 ] + "&lon=" + location[ 1 ]; - getTimeData( location, async function( weather ) { - - // Perform the HTTP request to retrieve the weather data - let forecast; - try { - forecast = await getData( forecastUrl ); - } catch (err) { - // Return just the time data if retrieving the forecast fails. - callback( weather ); - return; - } - - // Return just the time data if the forecast data is incomplete. - if ( !forecast || !forecast.list ) { - callback( weather ); - return; - } - - weather.temp = 0; - weather.humidity = 0; - weather.precip = 0; - - var periods = Math.min(forecast.list.length, 10); - for ( var index = 0; index < periods; index++ ) { - weather.temp += parseFloat( forecast.list[ index ].main.temp ); - weather.humidity += parseInt( forecast.list[ index ].main.humidity ); - weather.precip += ( forecast.list[ index ].rain ? parseFloat( forecast.list[ index ].rain[ "3h" ] || 0 ) : 0 ); - } - - weather.temp = weather.temp / periods; - weather.humidity = weather.humidity / periods; - weather.precip = weather.precip / 25.4; - weather.raining = ( forecast.list[ 0 ].rain ? ( parseFloat( forecast.list[ 0 ].rain[ "3h" ] || 0 ) > 0 ) : false ); + // TODO change the type of this after defining the appropriate type + const weather: any = getTimeData( location ); + // Perform the HTTP request to retrieve the weather data + let forecast; + try { + forecast = await getData( forecastUrl ); + } catch (err) { + // Return just the time data if retrieving the forecast fails. callback( weather ); - } ); + return; + } + + // Return just the time data if the forecast data is incomplete. + if ( !forecast || !forecast.list ) { + callback( weather ); + return; + } + + weather.temp = 0; + weather.humidity = 0; + weather.precip = 0; + + var periods = Math.min(forecast.list.length, 10); + for ( var index = 0; index < periods; index++ ) { + weather.temp += parseFloat( forecast.list[ index ].main.temp ); + weather.humidity += parseInt( forecast.list[ index ].main.humidity ); + weather.precip += ( forecast.list[ index ].rain ? parseFloat( forecast.list[ index ].rain[ "3h" ] || 0 ) : 0 ); + } + + weather.temp = weather.temp / periods; + weather.humidity = weather.humidity / periods; + weather.precip = weather.precip / 25.4; + weather.raining = ( forecast.list[ 0 ].rain ? ( parseFloat( forecast.list[ 0 ].rain[ "3h" ] || 0 ) > 0 ) : false ); + + callback( weather ); } // Retrieve weather data from Open Weather Map for App -function getOWMWeatherData( location, callback ) { +async function getOWMWeatherData( location, callback ) { var OWM_API_KEY = process.env.OWM_API_KEY, currentUrl = "http://api.openweathermap.org/data/2.5/weather?appid=" + OWM_API_KEY + "&units=imperial&lat=" + location[ 0 ] + "&lon=" + location[ 1 ], forecastDailyUrl = "http://api.openweathermap.org/data/2.5/forecast/daily?appid=" + OWM_API_KEY + "&units=imperial&lat=" + location[ 0 ] + "&lon=" + location[ 1 ]; - getTimeData( location, async function( weather ) { - - let current, forecast; - try { - current = await getData( currentUrl ); - forecast = await getData( forecastDailyUrl ); - } catch (err) { - // Return just the time data if retrieving weather data fails. - callback( weather ); - return; - } - - // Return just the time data if the weather data is incomplete. - if ( !current || !current.main || !current.wind || !current.weather || !forecast || !forecast.list ) { - callback( weather ); - return; - } - - weather.temp = parseInt( current.main.temp ); - weather.humidity = parseInt( current.main.humidity ); - weather.wind = parseInt( current.wind.speed ); - weather.description = current.weather[0].description; - weather.icon = current.weather[0].icon; - - weather.region = forecast.city.country; - weather.city = forecast.city.name; - weather.minTemp = parseInt( forecast.list[ 0 ].temp.min ); - weather.maxTemp = parseInt( forecast.list[ 0 ].temp.max ); - weather.precip = ( forecast.list[ 0 ].rain ? parseFloat( forecast.list[ 0 ].rain || 0 ) : 0 ) / 25.4; - weather.forecast = []; - - for ( var index = 0; index < forecast.list.length; index++ ) { - weather.forecast.push( { - temp_min: parseInt( forecast.list[ index ].temp.min ), - temp_max: parseInt( forecast.list[ index ].temp.max ), - date: parseInt( forecast.list[ index ].dt ), - icon: forecast.list[ index ].weather[ 0 ].icon, - description: forecast.list[ index ].weather[ 0 ].description - } ); - } + // TODO change the type of this after defining the appropriate type + const weather: any = getTimeData( location ); + let current, forecast; + try { + current = await getData( currentUrl ); + forecast = await getData( forecastDailyUrl ); + } catch (err) { + // Return just the time data if retrieving weather data fails. callback( weather ); - } ); + return; + } + + // Return just the time data if the weather data is incomplete. + if ( !current || !current.main || !current.wind || !current.weather || !forecast || !forecast.list ) { + callback( weather ); + return; + } + + weather.temp = parseInt( current.main.temp ); + weather.humidity = parseInt( current.main.humidity ); + weather.wind = parseInt( current.wind.speed ); + weather.description = current.weather[0].description; + weather.icon = current.weather[0].icon; + + weather.region = forecast.city.country; + weather.city = forecast.city.name; + weather.minTemp = parseInt( forecast.list[ 0 ].temp.min ); + weather.maxTemp = parseInt( forecast.list[ 0 ].temp.max ); + weather.precip = ( forecast.list[ 0 ].rain ? parseFloat( forecast.list[ 0 ].rain || 0 ) : 0 ) / 25.4; + weather.forecast = []; + + for ( var index = 0; index < forecast.list.length; index++ ) { + weather.forecast.push( { + temp_min: parseInt( forecast.list[ index ].temp.min ), + temp_max: parseInt( forecast.list[ index ].temp.max ), + date: parseInt( forecast.list[ index ].dt ), + icon: forecast.list[ index ].weather[ 0 ].icon, + description: forecast.list[ index ].weather[ 0 ].description + } ); + } + + callback( weather ); } -// Calculate timezone and sun rise/set information -function getTimeData( location, callback ) { - var timezone = moment().tz( geoTZ( location[ 0 ], location[ 1 ] ) ).utcOffset(); - var tzOffset = getTimezone( timezone, "minutes" ); +/** + * Calculates timezone and sunrise/sunset for the specified coordinates. + * @param coordinates The coordinates to use to calculate time data. + * @return The TimeData for the specified coordinates. + */ +function getTimeData( coordinates: GeoCoordinates ): TimeData { + const timezone = moment().tz( geoTZ( coordinates[ 0 ], coordinates[ 1 ] ) ).utcOffset(); + const tzOffset: number = getTimezone( timezone, true ); // Calculate sunrise and sunset since Weather Underground does not provide it - var sunData = SunCalc.getTimes( new Date(), location[ 0 ], location[ 1 ] ); + const sunData = SunCalc.getTimes( new Date(), coordinates[ 0 ], coordinates[ 1 ] ); sunData.sunrise.setUTCMinutes( sunData.sunrise.getUTCMinutes() + tzOffset ); sunData.sunset.setUTCMinutes( sunData.sunset.getUTCMinutes() + tzOffset ); - callback( { + return { timezone: timezone, sunrise: ( sunData.sunrise.getUTCHours() * 60 + sunData.sunrise.getUTCMinutes() ), sunset: ( sunData.sunset.getUTCHours() * 60 + sunData.sunset.getUTCMinutes() ) - } ); + }; } // Calculates the resulting water scale using the provided weather data, adjustment method and options @@ -294,7 +298,8 @@ exports.getWateringData = async function( req, res ) { finishRequest = function( weather ) { if ( !weather ) { if ( typeof location[ 0 ] === "number" && typeof location[ 1 ] === "number" ) { - getTimeData( location, finishRequest ); + const timeData: TimeData = getTimeData( location ); + finishRequest( timeData ); } else { res.send( "Error: No weather data found." ); } @@ -465,12 +470,16 @@ function validateValues( keys, array ) { return true; } -// 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. -function getTimezone( time, format ) { +/** + * Converts a timezone to an offset in minutes or OpenSprinkler encoded format. + * @param time A time string formatted in ISO-8601 or just the timezone. + * @param useMinutes Indicates if the returned value should be in minutes of the OpenSprinkler encoded format. + * @return The offset of the specified timezone in either minutes or OpenSprinkler encoded format (depending on the + * value of useMinutes). + */ +function getTimezone( time: number | string, useMinutes: boolean = false ): number { - var hour, minute, tz; + let hour, minute; if ( typeof time === "number" ) { hour = Math.floor( time / 60 ); @@ -478,24 +487,22 @@ function getTimezone( time, format ) { } else { // Match the provided time string against a regex for parsing - time = time.match( filters.time ) || time.match( filters.timezone ); + let splitTime = time.match( filters.time ) || time.match( filters.timezone ); - hour = parseInt( time[ 7 ] + time[ 8 ] ); - minute = parseInt( time[ 9 ] ); + hour = parseInt( splitTime[ 7 ] + splitTime[ 8 ] ); + minute = parseInt( splitTime[ 9 ] ); } - if ( format === "minutes" ) { - tz = ( hour * 60 ) + minute; + if ( useMinutes ) { + return ( hour * 60 ) + minute; } else { // Convert the timezone into the OpenSprinkler encoded format minute = ( minute / 15 >> 0 ) / 4; hour = hour + ( hour >= 0 ? minute : -minute ); - tz = ( ( hour + 12 ) * 4 ) >> 0; + return ( ( hour + 12 ) * 4 ) >> 0; } - - return tz; } // Converts IP string to integer @@ -506,3 +513,14 @@ function ipToInt( ip ) { /** Geographic coordinates. The 1st element is the latitude, and the 2nd element is the longitude. */ type GeoCoordinates = [number, number]; + +interface TimeData { + /** The UTC offset, in minutes. This uses POSIX offsets, which are the negation of typically used offsets + * (https://github.com/eggert/tz/blob/2017b/etcetera#L36-L42). + */ + timezone: number; + /** The time of sunrise, in minutes from UTC midnight. */ + sunrise: number; + /** The time of sunset, in minutes from UTC midnight. */ + sunset: number; +} From 4401781dfad513564ce67e164b0909aecd59179a Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Thu, 9 May 2019 16:29:47 -0400 Subject: [PATCH 5/9] Update getOWMWeatherData to modern TypeScript --- routes/weather.ts | 91 +++++++++++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 30 deletions(-) diff --git a/routes/weather.ts b/routes/weather.ts index aa075d6..91b9f3a 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -104,14 +104,18 @@ async function getOWMWateringData( location, callback ) { callback( weather ); } -// Retrieve weather data from Open Weather Map for App -async function getOWMWeatherData( location, callback ) { - var OWM_API_KEY = process.env.OWM_API_KEY, - currentUrl = "http://api.openweathermap.org/data/2.5/weather?appid=" + OWM_API_KEY + "&units=imperial&lat=" + location[ 0 ] + "&lon=" + location[ 1 ], - forecastDailyUrl = "http://api.openweathermap.org/data/2.5/forecast/daily?appid=" + OWM_API_KEY + "&units=imperial&lat=" + location[ 0 ] + "&lon=" + location[ 1 ]; +/** + * Retrieves the current weather data from OWM for usage in the mobile app. + * @param coordinates The coordinates to retrieve the weather for + * @return A Promise that will be resolved with the OWMWeatherData if the API calls succeed, or just the TimeData if + * an error occurs while retrieving the weather data. + */ +async function getOWMWeatherData( coordinates: GeoCoordinates ): Promise< OWMWeatherData | TimeData > { + const OWM_API_KEY = process.env.OWM_API_KEY, + currentUrl = "http://api.openweathermap.org/data/2.5/weather?appid=" + OWM_API_KEY + "&units=imperial&lat=" + coordinates[ 0 ] + "&lon=" + coordinates[ 1 ], + forecastDailyUrl = "http://api.openweathermap.org/data/2.5/forecast/daily?appid=" + OWM_API_KEY + "&units=imperial&lat=" + coordinates[ 0 ] + "&lon=" + coordinates[ 1 ]; - // TODO change the type of this after defining the appropriate type - const weather: any = getTimeData( location ); + const timeData: TimeData = getTimeData( coordinates ); let current, forecast; try { @@ -119,30 +123,31 @@ async function getOWMWeatherData( location, callback ) { forecast = await getData( forecastDailyUrl ); } catch (err) { // Return just the time data if retrieving weather data fails. - callback( weather ); - return; + return timeData; } // Return just the time data if the weather data is incomplete. if ( !current || !current.main || !current.wind || !current.weather || !forecast || !forecast.list ) { - callback( weather ); - return; + return timeData; } - weather.temp = parseInt( current.main.temp ); - weather.humidity = parseInt( current.main.humidity ); - weather.wind = parseInt( current.wind.speed ); - weather.description = current.weather[0].description; - weather.icon = current.weather[0].icon; + const weather: OWMWeatherData = { + ...timeData, + temp: parseInt( current.main.temp ), + humidity: parseInt( current.main.humidity ), + wind: parseInt( current.wind.speed ), + description: current.weather[0].description, + icon: current.weather[0].icon, - weather.region = forecast.city.country; - weather.city = forecast.city.name; - weather.minTemp = parseInt( forecast.list[ 0 ].temp.min ); - weather.maxTemp = parseInt( forecast.list[ 0 ].temp.max ); - weather.precip = ( forecast.list[ 0 ].rain ? parseFloat( forecast.list[ 0 ].rain || 0 ) : 0 ) / 25.4; - weather.forecast = []; + region: forecast.city.country, + city: forecast.city.name, + minTemp: parseInt( forecast.list[ 0 ].temp.min ), + maxTemp: parseInt( forecast.list[ 0 ].temp.max ), + precip: ( forecast.list[ 0 ].rain ? parseFloat( forecast.list[ 0 ].rain || 0 ) : 0 ) / 25.4, + forecast: [] + }; - for ( var index = 0; index < forecast.list.length; index++ ) { + for ( let index = 0; index < forecast.list.length; index++ ) { weather.forecast.push( { temp_min: parseInt( forecast.list[ index ].temp.min ), temp_max: parseInt( forecast.list[ index ].temp.max ), @@ -152,7 +157,7 @@ async function getOWMWeatherData( location, callback ) { } ); } - callback( weather ); + return weather; } /** @@ -254,9 +259,10 @@ exports.getWeatherData = async function( req, res ) { location = [ parseFloat( location[ 0 ] ), parseFloat( location[ 1 ] ) ]; // Continue with the weather request - getOWMWeatherData( location, function( data ) { - data.location = location; - res.json( data ); + const weatherData: OWMWeatherData | TimeData = await getOWMWeatherData( location ); + res.json( { + ...weatherData, + location: location } ); } else { @@ -271,9 +277,10 @@ exports.getWeatherData = async function( req, res ) { } location = coordinates; - getOWMWeatherData( location, function( data ) { - data.location = location; - res.json( data ); + const weatherData: OWMWeatherData | TimeData = await getOWMWeatherData( location ); + res.json( { + ...weatherData, + location: location } ); } }; @@ -524,3 +531,27 @@ interface TimeData { /** The time of sunset, in minutes from UTC midnight. */ sunset: number; } + +interface OWMWeatherData extends TimeData { + /** The current temperature (in Fahrenheit). */ + temp: number; + /** The current humidity (as a percentage). */ + humidity: number; + wind: number; + description: string; + icon: string; + region: string; + city: string; + minTemp: number; + maxTemp: number; + precip: number; + forecast: OWMWeatherDataForecast[] +} + +interface OWMWeatherDataForecast { + temp_min: number; + temp_max: number; + date: number; + icon: string; + description: string; +} From bef43c945375b550d54c82b944f35992c7727469 Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Thu, 9 May 2019 16:40:50 -0400 Subject: [PATCH 6/9] Update getOWMWateringData to modern TypeScript --- routes/weather.ts | 74 ++++++++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/routes/weather.ts b/routes/weather.ts index 91b9f3a..5e9f2de 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -61,13 +61,17 @@ async function getData( url: string ): Promise< any > { } } -// Retrieve data from Open Weather Map for water level calculations -async function getOWMWateringData( location, callback ) { - var OWM_API_KEY = process.env.OWM_API_KEY, - forecastUrl = "http://api.openweathermap.org/data/2.5/forecast?appid=" + OWM_API_KEY + "&units=imperial&lat=" + location[ 0 ] + "&lon=" + location[ 1 ]; +/** + * Retrieves weather data necessary for watering level calculations from the OWM API. + * @param coordinates The coordinates to retrieve the watering data for. + * @return A Promise that will be resolved with OWMWateringData if the API calls succeed, or just the TimeData if an + * error occurs while retrieving the weather data. + */ +async function getOWMWateringData( coordinates: GeoCoordinates ): Promise< OWMWateringData | TimeData > { + const OWM_API_KEY = process.env.OWM_API_KEY, + forecastUrl = "http://api.openweathermap.org/data/2.5/forecast?appid=" + OWM_API_KEY + "&units=imperial&lat=" + coordinates[ 0 ] + "&lon=" + coordinates[ 1 ]; - // TODO change the type of this after defining the appropriate type - const weather: any = getTimeData( location ); + const timeData: TimeData = getTimeData( coordinates ); // Perform the HTTP request to retrieve the weather data let forecast; @@ -75,33 +79,32 @@ async function getOWMWateringData( location, callback ) { forecast = await getData( forecastUrl ); } catch (err) { // Return just the time data if retrieving the forecast fails. - callback( weather ); - return; + return timeData; } // Return just the time data if the forecast data is incomplete. if ( !forecast || !forecast.list ) { - callback( weather ); - return; + return timeData; } - weather.temp = 0; - weather.humidity = 0; - weather.precip = 0; + let totalTemp = 0, + totalHumidity = 0, + totalPrecip = 0; - var periods = Math.min(forecast.list.length, 10); - for ( var index = 0; index < periods; index++ ) { - weather.temp += parseFloat( forecast.list[ index ].main.temp ); - weather.humidity += parseInt( forecast.list[ index ].main.humidity ); - weather.precip += ( forecast.list[ index ].rain ? parseFloat( forecast.list[ index ].rain[ "3h" ] || 0 ) : 0 ); + const periods = Math.min(forecast.list.length, 10); + for ( let index = 0; index < periods; index++ ) { + totalTemp += parseFloat( forecast.list[ index ].main.temp ); + totalHumidity += parseInt( forecast.list[ index ].main.humidity ); + totalPrecip += ( forecast.list[ index ].rain ? parseFloat( forecast.list[ index ].rain[ "3h" ] || 0 ) : 0 ); } - weather.temp = weather.temp / periods; - weather.humidity = weather.humidity / periods; - weather.precip = weather.precip / 25.4; - weather.raining = ( forecast.list[ 0 ].rain ? ( parseFloat( forecast.list[ 0 ].rain[ "3h" ] || 0 ) > 0 ) : false ); - - callback( weather ); + return { + ...timeData, + temp: totalTemp / periods, + humidity: totalHumidity / periods, + precip: totalPrecip / 25.4, + raining: ( forecast.list[ 0 ].rain ? ( parseFloat( forecast.list[ 0 ].rain[ "3h" ] || 0 ) > 0 ) : false ) + }; } /** @@ -390,6 +393,7 @@ exports.getWateringData = async function( req, res ) { adjustmentOptions = false; } + let coordinates: GeoCoordinates; // Parse location string if ( filters.pws.test( location ) ) { @@ -400,15 +404,13 @@ exports.getWateringData = async function( req, res ) { // Handle GPS coordinates by storing each coordinate in an array location = location.split( "," ); - location = [ parseFloat( location[ 0 ] ), parseFloat( location[ 1 ] ) ]; + coordinates = [ parseFloat( location[ 0 ] ), parseFloat( location[ 1 ] ) ]; + location = coordinates; - // 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 - let coordinates: GeoCoordinates; try { coordinates = await resolveCoordinates( location ); } catch (err) { @@ -417,8 +419,11 @@ exports.getWateringData = async function( req, res ) { } location = coordinates; - getOWMWateringData( location, finishRequest ); } + + // Continue with the weather request + const wateringData: OWMWateringData | TimeData = await getOWMWateringData( coordinates ); + finishRequest(wateringData); }; /** @@ -555,3 +560,14 @@ interface OWMWeatherDataForecast { icon: string; description: string; } + +interface OWMWateringData extends TimeData { + /** The average forecasted temperature over the next 30 hours (in Fahrenheit). */ + temp: number; + /** The average forecasted humidity over the next 30 hours (as a percentage). */ + humidity: number; + /** The forecasted total precipitation over the next 30 hours (in inches). */ + precip: number; + /** A boolean indicating if it is currently raining. */ + raining: boolean; +} From b11a12552888189d7b6e1cb1cf3db4c6c7fc9e46 Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Fri, 10 May 2019 12:57:22 -0400 Subject: [PATCH 7/9] Update getWeatherData to modern TypeScript --- routes/weather.ts | 51 +++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/routes/weather.ts b/routes/weather.ts index 5e9f2de..e0e8e35 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -1,3 +1,5 @@ +import * as express from "express"; + var http = require( "http" ), SunCalc = require( "suncalc" ), moment = require( "moment-timezone" ), @@ -252,40 +254,34 @@ function checkWeatherRestriction( adjustmentValue, weather ) { return false; } -exports.getWeatherData = async function( req, res ) { - var location = req.query.loc; +exports.getWeatherData = async function( req: express.Request, res: express.Response ) { + const location: string = getParameter(req.query.loc); + let coordinates: GeoCoordinates; 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 - const weatherData: OWMWeatherData | TimeData = await getOWMWeatherData( location ); - res.json( { - ...weatherData, - location: location - } ); + const split: string[] = location.split( "," ); + coordinates = [ parseFloat( split[ 0 ] ), parseFloat( split[ 1 ] ) ]; } 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 - let coordinates: GeoCoordinates; try { coordinates = await resolveCoordinates( location ); } catch (err) { res.send( "Error: Unable to resolve location" ); return; } - - location = coordinates; - const weatherData: OWMWeatherData | TimeData = await getOWMWeatherData( location ); - res.json( { - ...weatherData, - location: location - } ); } + + // Continue with the weather request + const weatherData: OWMWeatherData | TimeData = await getOWMWeatherData( coordinates ); + + res.json( { + ...weatherData, + location: coordinates + } ); }; // API Handler when using the weatherX.py where X represents the @@ -523,6 +519,23 @@ function ipToInt( ip ) { return ( ( ( ( ( ( +ip[ 0 ] ) * 256 ) + ( +ip[ 1 ] ) ) * 256 ) + ( +ip[ 2 ] ) ) * 256 ) + ( +ip[ 3 ] ); } +/** + * Returns a single value for a header/query parameter. If passed a single string, the same string will be returned. If + * an array of strings is passed, the first value will be returned. If this value is null/undefined, an empty string + * will be returned instead. + * @param parameter An array of parameters or a single parameter value. + * @return The first element in the array of parameter or the single parameter provided. + */ +function getParameter( parameter: string | string[] ): string { + if ( Array.isArray( parameter ) ) { + parameter = parameter[0]; + } + + // Return an empty string if the parameter is undefined. + return parameter || ""; +} + + /** Geographic coordinates. The 1st element is the latitude, and the 2nd element is the longitude. */ type GeoCoordinates = [number, number]; From d53307c6024e0ede1efea202f6509b620b4f0df2 Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Fri, 10 May 2019 18:49:45 -0400 Subject: [PATCH 8/9] Update remaining functions to modern TypeScript --- routes/weather.ts | 163 ++++++++++++++++++++++++++++++---------------- 1 file changed, 106 insertions(+), 57 deletions(-) diff --git a/routes/weather.ts b/routes/weather.ts index e0e8e35..4ac57c1 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -1,6 +1,6 @@ import * as express from "express"; -var http = require( "http" ), +const http = require( "http" ), SunCalc = require( "suncalc" ), moment = require( "moment-timezone" ), geoTZ = require( "geo-tz" ), @@ -187,18 +187,27 @@ function getTimeData( coordinates: GeoCoordinates ): TimeData { }; } -// Calculates the resulting water scale using the provided weather data, adjustment method and options -function calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ) { +/** + * Calculates how much watering should be scaled based on weather and adjustment options. + * @param adjustmentMethod The method to use to calculate the watering percentage. The only supported method is 1, which + * corresponds to the Zimmerman method. If an invalid adjustmentMethod is used, this method will return -1. + * @param adjustmentOptions Options to tweak the calculation, or undefined/null if no custom values are to be used. + * @param data The weather to use to calculate watering percentage. + * @return The percentage that watering should be scaled by, or -1 if an invalid adjustmentMethod was provided. + */ +function calculateWeatherScale( adjustmentMethod: number, adjustmentOptions: AdjustmentOptions, data: OWMWateringData | TimeData ): number { // Zimmerman method if ( adjustmentMethod === 1 ) { - var humidityBase = 30, tempBase = 70, precipBase = 0; + let humidityBase = 30, tempBase = 70, precipBase = 0; // Check to make sure valid data exists for all factors - if ( !validateValues( [ "temp", "humidity", "precip" ], weather ) ) { + if ( !validateValues( [ "temp", "humidity", "precip" ], data ) ) { return 100; } + const wateringData: OWMWateringData = data as OWMWateringData; + // Get baseline conditions for 100% water level, if provided if ( adjustmentOptions ) { humidityBase = adjustmentOptions.hasOwnProperty( "bh" ) ? adjustmentOptions.bh : humidityBase; @@ -206,10 +215,10 @@ 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 ), + let temp = wateringData.temp, + humidityFactor = ( humidityBase - wateringData.humidity ), tempFactor = ( ( temp - tempBase ) * 4 ), - precipFactor = ( ( precipBase - weather.precip ) * 200 ); + precipFactor = ( ( precipBase - wateringData.precip ) * 200 ); // Apply adjustment options, if provided, by multiplying the percentage against the factor if ( adjustmentOptions ) { @@ -233,17 +242,23 @@ function calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ) { return -1; } -// Checks if the weather data meets any of the restrictions set by OpenSprinkler. -// Restrictions prevent any watering from occurring and are similar to 0% watering level. -// -// California watering restriction prevents watering if precipitation over two days is greater -// than 0.01" over the past 48 hours. -function checkWeatherRestriction( adjustmentValue, weather ) { +/** + * Checks if the weather data meets any of the restrictions set by OpenSprinkler. Restrictions prevent any watering + * from occurring and are similar to 0% watering level. Known restrictions are: + * + * - California watering restriction prevents watering if precipitation over two days is greater than 0.1" over the past + * 48 hours. + * @param adjustmentValue The adjustment value, which indicates which restrictions should be checked. + * @param weather Watering data to use to determine if any restrictions apply. + * @return A boolean indicating if the watering level should be set to 0% due to a restriction. + */ +function checkWeatherRestriction( adjustmentValue: number, weather: OWMWateringData ): boolean { - var californiaRestriction = ( adjustmentValue >> 7 ) & 1; + const californiaRestriction = ( adjustmentValue >> 7 ) & 1; if ( californiaRestriction ) { + // TODO this is currently checking if the forecasted precipitation over the next 30 hours is >0.1 inches // If the California watering restriction is in use then prevent watering // if more then 0.1" of rain has accumulated in the past 48 hours if ( weather.precip > 0.1 ) { @@ -287,24 +302,25 @@ exports.getWeatherData = async function( req: express.Request, res: express.Resp // API Handler when using the weatherX.py where X represents the // adjustment method which is encoded to also carry the watering // restriction and therefore must be decoded -exports.getWateringData = async function( req, res ) { +exports.getWateringData = async function( req: express.Request, res: express.Response ) { // The adjustment method is encoded by the OpenSprinkler firmware and must be // parsed. This allows the adjustment method and the restriction type to both // be saved in the same byte. - var adjustmentMethod = req.params[ 0 ] & ~( 1 << 7 ), - adjustmentOptions = req.query.wto, - location = req.query.loc, - outputFormat = req.query.format, - remoteAddress = req.headers[ "x-forwarded-for" ] || req.connection.remoteAddress, + let adjustmentMethod: number = req.params[ 0 ] & ~( 1 << 7 ), + adjustmentOptionsString: string = getParameter(req.query.wto), + location: string | GeoCoordinates = getParameter(req.query.loc), + outputFormat: string = getParameter(req.query.format), + remoteAddress: string = getParameter(req.headers[ "x-forwarded-for" ]) || req.connection.remoteAddress, + adjustmentOptions: AdjustmentOptions, // Function that will accept the weather after it is received from the API // Data will be processed to retrieve the resulting scale, sunrise/sunset, timezone, // and also calculate if a restriction is met to prevent watering. - finishRequest = function( weather ) { + finishRequest = function( weather: OWMWateringData | TimeData ) { if ( !weather ) { if ( typeof location[ 0 ] === "number" && typeof location[ 1 ] === "number" ) { - const timeData: TimeData = getTimeData( location ); + const timeData: TimeData = getTimeData( location as GeoCoordinates ); finishRequest( timeData ); } else { res.send( "Error: No weather data found." ); @@ -313,16 +329,22 @@ exports.getWateringData = async function( req, res ) { return; } - var scale = calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ), - rainDelay = -1; + // The OWMWateringData if it exists, or undefined if only TimeData is available + const wateringData: OWMWateringData = validateValues( [ "temp", "humidity", "precip" ], weather ) ? weather as OWMWateringData: undefined; - // Check for any user-set restrictions and change the scale to 0 if the criteria is met - if ( checkWeatherRestriction( req.params[ 0 ], weather ) ) { - scale = 0; + + let scale: number = calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ), + rainDelay: number = -1; + + if (wateringData) { + // Check for any user-set restrictions and change the scale to 0 if the criteria is met + if (checkWeatherRestriction(req.params[0], wateringData)) { + scale = 0; + } } // If any weather adjustment is being used, check the rain status - if ( adjustmentMethod > 0 && weather.hasOwnProperty( "raining" ) && weather.raining ) { + if ( adjustmentMethod > 0 && wateringData && wateringData.raining ) { // If it is raining and the user has weather-based rain delay as the adjustment method then apply the specified delay if ( adjustmentMethod === 2 ) { @@ -335,20 +357,21 @@ exports.getWateringData = async function( req, res ) { } } - var data = { - scale: scale, - rd: rainDelay, - tz: getTimezone( weather.timezone, undefined ), - sunrise: weather.sunrise, - sunset: weather.sunset, - eip: ipToInt( remoteAddress ), - rawData: { - h: weather.humidity, - p: Math.round( weather.precip * 100 ) / 100, - t: Math.round( weather.temp * 10 ) / 10, - raining: weather.raining ? 1 : 0 - } - }; + const data = { + scale: scale, + rd: rainDelay, + tz: getTimezone( weather.timezone, undefined ), + sunrise: weather.sunrise, + sunset: weather.sunset, + eip: ipToInt( remoteAddress ), + // TODO this may need to be changed (https://github.com/OpenSprinkler/OpenSprinkler-Weather/pull/11#issuecomment-491037948) + rawData: { + h: wateringData ? wateringData.humidity : null, + p: wateringData ? Math.round( wateringData.precip * 100 ) / 100 : null, + t: wateringData ? Math.round( wateringData.temp * 10 ) / 10 : null, + raining: wateringData ? ( wateringData.raining ? 1 : 0 ) : null + } + }; // Return the response to the client in the requested format if ( outputFormat === "json" ) { @@ -379,14 +402,14 @@ exports.getWateringData = async function( req, res ) { try { // Parse data that may be encoded - adjustmentOptions = decodeURIComponent( adjustmentOptions.replace( /\\x/g, "%" ) ); + adjustmentOptionsString = decodeURIComponent( adjustmentOptionsString.replace( /\\x/g, "%" ) ); // Reconstruct JSON string from deformed controller output - adjustmentOptions = JSON.parse( "{" + adjustmentOptions + "}" ); + adjustmentOptions = JSON.parse( "{" + adjustmentOptionsString + "}" ); } catch ( err ) { // If the JSON is not valid, do not incorporate weather adjustment options - adjustmentOptions = false; + adjustmentOptions = undefined; } let coordinates: GeoCoordinates; @@ -399,8 +422,8 @@ exports.getWateringData = async function( req, res ) { } else if ( filters.gps.test( location ) ) { // Handle GPS coordinates by storing each coordinate in an array - location = location.split( "," ); - coordinates = [ parseFloat( location[ 0 ] ), parseFloat( location[ 1 ] ) ]; + const splitLocation: string[] = location.split( "," ); + coordinates = [ parseFloat( splitLocation[ 0 ] ), parseFloat( splitLocation[ 1 ] ) ]; location = coordinates; } else { @@ -419,7 +442,7 @@ exports.getWateringData = async function( req, res ) { // Continue with the weather request const wateringData: OWMWateringData | TimeData = await getOWMWateringData( coordinates ); - finishRequest(wateringData); + finishRequest( wateringData ); }; /** @@ -459,9 +482,14 @@ async function httpRequest( url: string ): Promise< string > { } ); } -// Checks to make sure an array contains the keys provided and returns true or false -function validateValues( keys, array ) { - var key; +/** + * Checks if the specified object contains numeric values for each of the specified keys. + * @param keys A list of keys to validate exist on the specified object. + * @param obj The object to check. + * @return A boolean indicating if the object has numeric values for all of the specified keys. + */ +function validateValues( keys: string[], obj: object ): boolean { + let key: string; for ( key in keys ) { if ( !keys.hasOwnProperty( key ) ) { @@ -470,7 +498,7 @@ function validateValues( keys, array ) { key = keys[ key ]; - if ( !array.hasOwnProperty( key ) || typeof array[ key ] !== "number" || isNaN( array[ key ] ) || array[ key ] === null || array[ key ] === -999 ) { + if ( !obj.hasOwnProperty( key ) || typeof obj[ key ] !== "number" || isNaN( obj[ key ] ) || obj[ key ] === null || obj[ key ] === -999 ) { return false; } } @@ -513,10 +541,14 @@ function getTimezone( time: number | string, useMinutes: boolean = false ): numb } } -// Converts IP string to integer -function ipToInt( ip ) { - ip = ip.split( "." ); - return ( ( ( ( ( ( +ip[ 0 ] ) * 256 ) + ( +ip[ 1 ] ) ) * 256 ) + ( +ip[ 2 ] ) ) * 256 ) + ( +ip[ 3 ] ); +/** + * Converts an IP address string to an integer. + * @param ip The string representation of the IP address. + * @return The integer representation of the IP address. + */ +function ipToInt( ip: string ): number { + const split = ip.split( "." ); + return ( ( ( ( ( ( +split[ 0 ] ) * 256 ) + ( +split[ 1 ] ) ) * 256 ) + ( +split[ 2 ] ) ) * 256 ) + ( +split[ 3 ] ); } /** @@ -584,3 +616,20 @@ interface OWMWateringData extends TimeData { /** A boolean indicating if it is currently raining. */ raining: boolean; } + +interface AdjustmentOptions { + /** Base humidity (as a percentage). */ + bh?: number; + /** Base temperature (in Fahrenheit). */ + bt?: number; + /** Base precipitation (in inches). */ + br?: number; + /** The percentage to weight the humidity factor by. */ + h?: number; + /** The percentage to weight the temperature factor by. */ + t?: number; + /** The percentage to weight the precipitation factor by. */ + r?: number; + /** The rain delay to use (in hours). */ + d?: number; +} From 08211acbc895f1cbea193523d30b7f182dc6fbb5 Mon Sep 17 00:00:00 2001 From: Matthew Oslan Date: Fri, 10 May 2019 19:30:40 -0400 Subject: [PATCH 9/9] Separate OWMWeatherData and OWMWateringData from TimeData --- routes/weather.ts | 195 ++++++++++++++++++++++------------------------ 1 file changed, 93 insertions(+), 102 deletions(-) diff --git a/routes/weather.ts b/routes/weather.ts index 4ac57c1..d8770f1 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -66,27 +66,25 @@ async function getData( url: string ): Promise< any > { /** * Retrieves weather data necessary for watering level calculations from the OWM API. * @param coordinates The coordinates to retrieve the watering data for. - * @return A Promise that will be resolved with OWMWateringData if the API calls succeed, or just the TimeData if an + * @return A Promise that will be resolved with OWMWateringData if the API calls succeed, or undefined if an * error occurs while retrieving the weather data. */ -async function getOWMWateringData( coordinates: GeoCoordinates ): Promise< OWMWateringData | TimeData > { +async function getOWMWateringData( coordinates: GeoCoordinates ): Promise< OWMWateringData > { const OWM_API_KEY = process.env.OWM_API_KEY, forecastUrl = "http://api.openweathermap.org/data/2.5/forecast?appid=" + OWM_API_KEY + "&units=imperial&lat=" + coordinates[ 0 ] + "&lon=" + coordinates[ 1 ]; - const timeData: TimeData = getTimeData( coordinates ); - // Perform the HTTP request to retrieve the weather data let forecast; try { forecast = await getData( forecastUrl ); } catch (err) { - // Return just the time data if retrieving the forecast fails. - return timeData; + // Indicate watering data could not be retrieved if an API error occurs. + return undefined; } - // Return just the time data if the forecast data is incomplete. + // Indicate watering data could not be retrieved if the forecast data is incomplete. if ( !forecast || !forecast.list ) { - return timeData; + return undefined; } let totalTemp = 0, @@ -101,7 +99,6 @@ async function getOWMWateringData( coordinates: GeoCoordinates ): Promise< OWMWa } return { - ...timeData, temp: totalTemp / periods, humidity: totalHumidity / periods, precip: totalPrecip / 25.4, @@ -112,32 +109,29 @@ async function getOWMWateringData( coordinates: GeoCoordinates ): Promise< OWMWa /** * Retrieves the current weather data from OWM for usage in the mobile app. * @param coordinates The coordinates to retrieve the weather for - * @return A Promise that will be resolved with the OWMWeatherData if the API calls succeed, or just the TimeData if + * @return A Promise that will be resolved with the OWMWeatherData if the API calls succeed, or undefined if * an error occurs while retrieving the weather data. */ -async function getOWMWeatherData( coordinates: GeoCoordinates ): Promise< OWMWeatherData | TimeData > { +async function getOWMWeatherData( coordinates: GeoCoordinates ): Promise< OWMWeatherData > { const OWM_API_KEY = process.env.OWM_API_KEY, currentUrl = "http://api.openweathermap.org/data/2.5/weather?appid=" + OWM_API_KEY + "&units=imperial&lat=" + coordinates[ 0 ] + "&lon=" + coordinates[ 1 ], forecastDailyUrl = "http://api.openweathermap.org/data/2.5/forecast/daily?appid=" + OWM_API_KEY + "&units=imperial&lat=" + coordinates[ 0 ] + "&lon=" + coordinates[ 1 ]; - const timeData: TimeData = getTimeData( coordinates ); - let current, forecast; try { current = await getData( currentUrl ); forecast = await getData( forecastDailyUrl ); } catch (err) { - // Return just the time data if retrieving weather data fails. - return timeData; + // Indicate watering data could not be retrieved if an API error occurs. + return undefined; } - // Return just the time data if the weather data is incomplete. + // Indicate watering data could not be retrieved if the forecast data is incomplete. if ( !current || !current.main || !current.wind || !current.weather || !forecast || !forecast.list ) { - return timeData; + return undefined; } const weather: OWMWeatherData = { - ...timeData, temp: parseInt( current.main.temp ), humidity: parseInt( current.main.humidity ), wind: parseInt( current.wind.speed ), @@ -192,22 +186,20 @@ function getTimeData( coordinates: GeoCoordinates ): TimeData { * @param adjustmentMethod The method to use to calculate the watering percentage. The only supported method is 1, which * corresponds to the Zimmerman method. If an invalid adjustmentMethod is used, this method will return -1. * @param adjustmentOptions Options to tweak the calculation, or undefined/null if no custom values are to be used. - * @param data The weather to use to calculate watering percentage. + * @param wateringData The weather to use to calculate watering percentage. * @return The percentage that watering should be scaled by, or -1 if an invalid adjustmentMethod was provided. */ -function calculateWeatherScale( adjustmentMethod: number, adjustmentOptions: AdjustmentOptions, data: OWMWateringData | TimeData ): number { +function calculateWeatherScale( adjustmentMethod: number, adjustmentOptions: AdjustmentOptions, wateringData: OWMWateringData ): number { // Zimmerman method if ( adjustmentMethod === 1 ) { let humidityBase = 30, tempBase = 70, precipBase = 0; // Check to make sure valid data exists for all factors - if ( !validateValues( [ "temp", "humidity", "precip" ], data ) ) { + if ( !validateValues( [ "temp", "humidity", "precip" ], wateringData ) ) { return 100; } - const wateringData: OWMWateringData = data as OWMWateringData; - // Get baseline conditions for 100% water level, if provided if ( adjustmentOptions ) { humidityBase = adjustmentOptions.hasOwnProperty( "bh" ) ? adjustmentOptions.bh : humidityBase; @@ -291,9 +283,11 @@ exports.getWeatherData = async function( req: express.Request, res: express.Resp } // Continue with the weather request - const weatherData: OWMWeatherData | TimeData = await getOWMWeatherData( coordinates ); + const timeData: TimeData = getTimeData( coordinates ); + const weatherData: OWMWeatherData = await getOWMWeatherData( coordinates ); res.json( { + ...timeData, ...weatherData, location: coordinates } ); @@ -312,81 +306,8 @@ exports.getWateringData = async function( req: express.Request, res: express.Res location: string | GeoCoordinates = getParameter(req.query.loc), outputFormat: string = getParameter(req.query.format), remoteAddress: string = getParameter(req.headers[ "x-forwarded-for" ]) || req.connection.remoteAddress, - adjustmentOptions: AdjustmentOptions, + adjustmentOptions: AdjustmentOptions; - // Function that will accept the weather after it is received from the API - // Data will be processed to retrieve the resulting scale, sunrise/sunset, timezone, - // and also calculate if a restriction is met to prevent watering. - finishRequest = function( weather: OWMWateringData | TimeData ) { - if ( !weather ) { - if ( typeof location[ 0 ] === "number" && typeof location[ 1 ] === "number" ) { - const timeData: TimeData = getTimeData( location as GeoCoordinates ); - finishRequest( timeData ); - } else { - res.send( "Error: No weather data found." ); - } - - return; - } - - // The OWMWateringData if it exists, or undefined if only TimeData is available - const wateringData: OWMWateringData = validateValues( [ "temp", "humidity", "precip" ], weather ) ? weather as OWMWateringData: undefined; - - - let scale: number = calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ), - rainDelay: number = -1; - - if (wateringData) { - // Check for any user-set restrictions and change the scale to 0 if the criteria is met - if (checkWeatherRestriction(req.params[0], wateringData)) { - scale = 0; - } - } - - // If any weather adjustment is being used, check the rain status - if ( adjustmentMethod > 0 && wateringData && wateringData.raining ) { - - // If it is raining and the user has weather-based rain delay as the adjustment method then apply the specified delay - if ( adjustmentMethod === 2 ) { - - rainDelay = ( adjustmentOptions && adjustmentOptions.hasOwnProperty( "d" ) ) ? adjustmentOptions.d : 24; - } else { - - // For any other adjustment method, apply a scale of 0 (as the scale will revert when the rain stops) - scale = 0; - } - } - - const data = { - scale: scale, - rd: rainDelay, - tz: getTimezone( weather.timezone, undefined ), - sunrise: weather.sunrise, - sunset: weather.sunset, - eip: ipToInt( remoteAddress ), - // TODO this may need to be changed (https://github.com/OpenSprinkler/OpenSprinkler-Weather/pull/11#issuecomment-491037948) - rawData: { - h: wateringData ? wateringData.humidity : null, - p: wateringData ? Math.round( wateringData.precip * 100 ) / 100 : null, - t: wateringData ? Math.round( wateringData.temp * 10 ) / 10 : null, - raining: wateringData ? ( wateringData.raining ? 1 : 0 ) : null - } - }; - - // Return the response to the client in the requested format - if ( outputFormat === "json" ) { - res.json( data ); - } else { - res.send( "&scale=" + data.scale + - "&rd=" + data.rd + - "&tz=" + data.tz + - "&sunrise=" + data.sunrise + - "&sunset=" + data.sunset + - "&eip=" + data.eip + - "&rawData=" + JSON.stringify( data.rawData ) - ); - } - }; // Exit if no location is provided if ( !location ) { @@ -441,8 +362,78 @@ exports.getWateringData = async function( req: express.Request, res: express.Res } // Continue with the weather request - const wateringData: OWMWateringData | TimeData = await getOWMWateringData( coordinates ); - finishRequest( wateringData ); + let timeData: TimeData = getTimeData( coordinates ); + const wateringData: OWMWateringData = await getOWMWateringData( coordinates ); + + + // Process data to retrieve the resulting scale, sunrise/sunset, timezone, + // and also calculate if a restriction is met to prevent watering. + + // Use getTimeData as fallback if a PWS is used but time data is not provided. + // This will never occur, but it might become possible in the future when PWS support is re-added. + if ( !timeData ) { + if ( typeof location[ 0 ] === "number" && typeof location[ 1 ] === "number" ) { + timeData = getTimeData( location as GeoCoordinates ); + } else { + res.send( "Error: No weather data found." ); + return; + } + } + + let scale: number = calculateWeatherScale( adjustmentMethod, adjustmentOptions, wateringData ), + rainDelay: number = -1; + + if (wateringData) { + // Check for any user-set restrictions and change the scale to 0 if the criteria is met + if (checkWeatherRestriction(req.params[0], wateringData)) { + scale = 0; + } + } + + // If any weather adjustment is being used, check the rain status + if ( adjustmentMethod > 0 && wateringData && wateringData.raining ) { + + // If it is raining and the user has weather-based rain delay as the adjustment method then apply the specified delay + if ( adjustmentMethod === 2 ) { + + rainDelay = ( adjustmentOptions && adjustmentOptions.hasOwnProperty( "d" ) ) ? adjustmentOptions.d : 24; + } else { + + // For any other adjustment method, apply a scale of 0 (as the scale will revert when the rain stops) + scale = 0; + } + } + + const data = { + scale: scale, + rd: rainDelay, + tz: getTimezone( timeData.timezone, undefined ), + sunrise: timeData.sunrise, + sunset: timeData.sunset, + eip: ipToInt( remoteAddress ), + // TODO this may need to be changed (https://github.com/OpenSprinkler/OpenSprinkler-Weather/pull/11#issuecomment-491037948) + rawData: { + h: wateringData ? wateringData.humidity : null, + p: wateringData ? Math.round( wateringData.precip * 100 ) / 100 : null, + t: wateringData ? Math.round( wateringData.temp * 10 ) / 10 : null, + raining: wateringData ? ( wateringData.raining ? 1 : 0 ) : null + } + }; + + // Return the response to the client in the requested format + if ( outputFormat === "json" ) { + res.json( data ); + } else { + res.send( "&scale=" + data.scale + + "&rd=" + data.rd + + "&tz=" + data.tz + + "&sunrise=" + data.sunrise + + "&sunset=" + data.sunset + + "&eip=" + data.eip + + "&rawData=" + JSON.stringify( data.rawData ) + ); + } + }; /** @@ -582,7 +573,7 @@ interface TimeData { sunset: number; } -interface OWMWeatherData extends TimeData { +interface OWMWeatherData { /** The current temperature (in Fahrenheit). */ temp: number; /** The current humidity (as a percentage). */ @@ -606,7 +597,7 @@ interface OWMWeatherDataForecast { description: string; } -interface OWMWateringData extends TimeData { +interface OWMWateringData { /** The average forecasted temperature over the next 30 hours (in Fahrenheit). */ temp: number; /** The average forecasted humidity over the next 30 hours (as a percentage). */