Update remaining functions to modern TypeScript

This commit is contained in:
Matthew Oslan
2019-05-10 18:49:45 -04:00
parent b11a125528
commit d53307c602

View File

@@ -1,6 +1,6 @@
import * as express from "express"; import * as express from "express";
var http = require( "http" ), const http = require( "http" ),
SunCalc = require( "suncalc" ), SunCalc = require( "suncalc" ),
moment = require( "moment-timezone" ), moment = require( "moment-timezone" ),
geoTZ = require( "geo-tz" ), 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 // Zimmerman method
if ( adjustmentMethod === 1 ) { 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 // Check to make sure valid data exists for all factors
if ( !validateValues( [ "temp", "humidity", "precip" ], weather ) ) { if ( !validateValues( [ "temp", "humidity", "precip" ], data ) ) {
return 100; return 100;
} }
const wateringData: OWMWateringData = data as OWMWateringData;
// Get baseline conditions for 100% water level, if provided // Get baseline conditions for 100% water level, if provided
if ( adjustmentOptions ) { if ( adjustmentOptions ) {
humidityBase = adjustmentOptions.hasOwnProperty( "bh" ) ? adjustmentOptions.bh : humidityBase; humidityBase = adjustmentOptions.hasOwnProperty( "bh" ) ? adjustmentOptions.bh : humidityBase;
@@ -206,10 +215,10 @@ function calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ) {
precipBase = adjustmentOptions.hasOwnProperty( "br" ) ? adjustmentOptions.br : precipBase; precipBase = adjustmentOptions.hasOwnProperty( "br" ) ? adjustmentOptions.br : precipBase;
} }
var temp = ( ( weather.maxTemp + weather.minTemp ) / 2 ) || weather.temp, let temp = wateringData.temp,
humidityFactor = ( humidityBase - weather.humidity ), humidityFactor = ( humidityBase - wateringData.humidity ),
tempFactor = ( ( temp - tempBase ) * 4 ), 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 // Apply adjustment options, if provided, by multiplying the percentage against the factor
if ( adjustmentOptions ) { if ( adjustmentOptions ) {
@@ -233,17 +242,23 @@ function calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ) {
return -1; 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. * 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.01" over the past 48 hours. * - California watering restriction prevents watering if precipitation over two days is greater than 0.1" over the past
function checkWeatherRestriction( adjustmentValue, weather ) { * 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 ) { 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 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 more then 0.1" of rain has accumulated in the past 48 hours
if ( weather.precip > 0.1 ) { 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 // API Handler when using the weatherX.py where X represents the
// adjustment method which is encoded to also carry the watering // adjustment method which is encoded to also carry the watering
// restriction and therefore must be decoded // 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 // The adjustment method is encoded by the OpenSprinkler firmware and must be
// parsed. This allows the adjustment method and the restriction type to both // parsed. This allows the adjustment method and the restriction type to both
// be saved in the same byte. // be saved in the same byte.
var adjustmentMethod = req.params[ 0 ] & ~( 1 << 7 ), let adjustmentMethod: number = req.params[ 0 ] & ~( 1 << 7 ),
adjustmentOptions = req.query.wto, adjustmentOptionsString: string = getParameter(req.query.wto),
location = req.query.loc, location: string | GeoCoordinates = getParameter(req.query.loc),
outputFormat = req.query.format, outputFormat: string = getParameter(req.query.format),
remoteAddress = req.headers[ "x-forwarded-for" ] || req.connection.remoteAddress, 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 // 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, // Data will be processed to retrieve the resulting scale, sunrise/sunset, timezone,
// and also calculate if a restriction is met to prevent watering. // and also calculate if a restriction is met to prevent watering.
finishRequest = function( weather ) { finishRequest = function( weather: OWMWateringData | TimeData ) {
if ( !weather ) { if ( !weather ) {
if ( typeof location[ 0 ] === "number" && typeof location[ 1 ] === "number" ) { if ( typeof location[ 0 ] === "number" && typeof location[ 1 ] === "number" ) {
const timeData: TimeData = getTimeData( location ); const timeData: TimeData = getTimeData( location as GeoCoordinates );
finishRequest( timeData ); finishRequest( timeData );
} else { } else {
res.send( "Error: No weather data found." ); res.send( "Error: No weather data found." );
@@ -313,16 +329,22 @@ exports.getWateringData = async function( req, res ) {
return; return;
} }
var scale = calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ), // The OWMWateringData if it exists, or undefined if only TimeData is available
rainDelay = -1; 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 // Check for any user-set restrictions and change the scale to 0 if the criteria is met
if ( checkWeatherRestriction( req.params[ 0 ], weather ) ) { if (checkWeatherRestriction(req.params[0], wateringData)) {
scale = 0; scale = 0;
} }
}
// If any weather adjustment is being used, check the rain status // 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 it is raining and the user has weather-based rain delay as the adjustment method then apply the specified delay
if ( adjustmentMethod === 2 ) { if ( adjustmentMethod === 2 ) {
@@ -335,18 +357,19 @@ exports.getWateringData = async function( req, res ) {
} }
} }
var data = { const data = {
scale: scale, scale: scale,
rd: rainDelay, rd: rainDelay,
tz: getTimezone( weather.timezone, undefined ), tz: getTimezone( weather.timezone, undefined ),
sunrise: weather.sunrise, sunrise: weather.sunrise,
sunset: weather.sunset, sunset: weather.sunset,
eip: ipToInt( remoteAddress ), eip: ipToInt( remoteAddress ),
// TODO this may need to be changed (https://github.com/OpenSprinkler/OpenSprinkler-Weather/pull/11#issuecomment-491037948)
rawData: { rawData: {
h: weather.humidity, h: wateringData ? wateringData.humidity : null,
p: Math.round( weather.precip * 100 ) / 100, p: wateringData ? Math.round( wateringData.precip * 100 ) / 100 : null,
t: Math.round( weather.temp * 10 ) / 10, t: wateringData ? Math.round( wateringData.temp * 10 ) / 10 : null,
raining: weather.raining ? 1 : 0 raining: wateringData ? ( wateringData.raining ? 1 : 0 ) : null
} }
}; };
@@ -379,14 +402,14 @@ exports.getWateringData = async function( req, res ) {
try { try {
// Parse data that may be encoded // 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 // Reconstruct JSON string from deformed controller output
adjustmentOptions = JSON.parse( "{" + adjustmentOptions + "}" ); adjustmentOptions = JSON.parse( "{" + adjustmentOptionsString + "}" );
} catch ( err ) { } catch ( err ) {
// If the JSON is not valid, do not incorporate weather adjustment options // If the JSON is not valid, do not incorporate weather adjustment options
adjustmentOptions = false; adjustmentOptions = undefined;
} }
let coordinates: GeoCoordinates; let coordinates: GeoCoordinates;
@@ -399,8 +422,8 @@ exports.getWateringData = async function( req, res ) {
} else if ( filters.gps.test( location ) ) { } else if ( filters.gps.test( location ) ) {
// Handle GPS coordinates by storing each coordinate in an array // Handle GPS coordinates by storing each coordinate in an array
location = location.split( "," ); const splitLocation: string[] = location.split( "," );
coordinates = [ parseFloat( location[ 0 ] ), parseFloat( location[ 1 ] ) ]; coordinates = [ parseFloat( splitLocation[ 0 ] ), parseFloat( splitLocation[ 1 ] ) ];
location = coordinates; location = coordinates;
} else { } else {
@@ -419,7 +442,7 @@ exports.getWateringData = async function( req, res ) {
// Continue with the weather request // Continue with the weather request
const wateringData: OWMWateringData | TimeData = await getOWMWateringData( coordinates ); 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 ) { * Checks if the specified object contains numeric values for each of the specified keys.
var key; * @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 ) { for ( key in keys ) {
if ( !keys.hasOwnProperty( key ) ) { if ( !keys.hasOwnProperty( key ) ) {
@@ -470,7 +498,7 @@ function validateValues( keys, array ) {
key = keys[ key ]; 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; return false;
} }
} }
@@ -513,10 +541,14 @@ function getTimezone( time: number | string, useMinutes: boolean = false ): numb
} }
} }
// Converts IP string to integer /**
function ipToInt( ip ) { * Converts an IP address string to an integer.
ip = ip.split( "." ); * @param ip The string representation of the IP address.
return ( ( ( ( ( ( +ip[ 0 ] ) * 256 ) + ( +ip[ 1 ] ) ) * 256 ) + ( +ip[ 2 ] ) ) * 256 ) + ( +ip[ 3 ] ); * @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. */ /** A boolean indicating if it is currently raining. */
raining: boolean; 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;
}