Merge pull request #81 from Derpthemeus/add-error-codes
Add error codes to watering data errors
This commit is contained in:
85
errors.ts
Normal file
85
errors.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
export enum ErrorCode {
|
||||||
|
/** No error occurred. This code should be included with all successful responses because the firmware expects some
|
||||||
|
* code to be present.
|
||||||
|
*/
|
||||||
|
NoError = 0,
|
||||||
|
|
||||||
|
/** The watering scale could not be calculated due to a problem with the weather information. */
|
||||||
|
BadWeatherData = 1,
|
||||||
|
/** Data for a full 24 hour period was not available. */
|
||||||
|
InsufficientWeatherData = 10,
|
||||||
|
/** A necessary field was missing from weather data returned by the API. */
|
||||||
|
MissingWeatherField = 11,
|
||||||
|
/** An HTTP or parsing error occurred when retrieving weather information. */
|
||||||
|
WeatherApiError = 12,
|
||||||
|
|
||||||
|
/** The specified location name could not be resolved. */
|
||||||
|
LocationError = 2,
|
||||||
|
/** An HTTP or parsing error occurred when resolving the location. */
|
||||||
|
LocationServiceApiError = 20,
|
||||||
|
/** No matches were found for the specified location name. */
|
||||||
|
NoLocationFound = 21,
|
||||||
|
/** The location name was specified in an invalid format (e.g. a PWS ID). */
|
||||||
|
InvalidLocationFormat = 22,
|
||||||
|
|
||||||
|
/** An Error related to personal weather stations. */
|
||||||
|
PwsError = 3,
|
||||||
|
/** The PWS ID did not use the correct format. */
|
||||||
|
InvalidPwsId = 30,
|
||||||
|
/** The PWS API key did not use the correct format. */
|
||||||
|
InvalidPwsApiKey = 31,
|
||||||
|
// TODO use this error code.
|
||||||
|
/** The PWS API returned an error because a bad API key was specified. */
|
||||||
|
PwsAuthenticationError = 32,
|
||||||
|
/** A PWS was specified but the data for the specified AdjustmentMethod cannot be retrieved from a PWS. */
|
||||||
|
PwsNotSupported = 33,
|
||||||
|
/** A PWS is required by the WeatherProvider but was not provided. */
|
||||||
|
NoPwsProvided = 34,
|
||||||
|
|
||||||
|
/** An error related to AdjustmentMethods or watering restrictions. */
|
||||||
|
AdjustmentMethodError = 4,
|
||||||
|
/** The WeatherProvider is incompatible with the specified AdjustmentMethod. */
|
||||||
|
UnsupportedAdjustmentMethod = 40,
|
||||||
|
/** An invalid AdjustmentMethod ID was specified. */
|
||||||
|
InvalidAdjustmentMethod = 41,
|
||||||
|
|
||||||
|
/** An error related to adjustment options (wto). */
|
||||||
|
AdjustmentOptionsError = 5,
|
||||||
|
/** The adjustment options could not be parsed. */
|
||||||
|
MalformedAdjustmentOptions = 50,
|
||||||
|
/** A required adjustment option was not provided. */
|
||||||
|
MissingAdjustmentOption = 51,
|
||||||
|
|
||||||
|
/** An error was not properly handled and assigned a more specific error code. */
|
||||||
|
UnexpectedError = 99
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An error with a numeric code that can be used to identify the type of error. */
|
||||||
|
export class CodedError extends Error {
|
||||||
|
public readonly errCode: ErrorCode;
|
||||||
|
|
||||||
|
public constructor( errCode: ErrorCode, message?: string ) {
|
||||||
|
super( message );
|
||||||
|
// https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
||||||
|
Object.setPrototypeOf( this, CodedError.prototype );
|
||||||
|
this.errCode = errCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a CodedError representing the specified error. This function can be used to ensure that errors caught in try-catch
|
||||||
|
* statements have an error code and do not contain any sensitive information in the error message. If `err` is a
|
||||||
|
* CodedError, the same object will be returned. If `err` is not a CodedError, it is assumed that the error wasn't
|
||||||
|
* properly handled, so a CodedError with a generic message and an "UnexpectedError" code will be returned. This ensures
|
||||||
|
* that the user will only be sent errors that were initially raised by the OpenSprinkler weather service and have
|
||||||
|
* had any sensitive information (like API keys) removed from the error message.
|
||||||
|
* @param err Any error caught in a try-catch statement.
|
||||||
|
* @return A CodedError representing the error that was passed to the function.
|
||||||
|
*/
|
||||||
|
export function makeCodedError( err: any ): CodedError {
|
||||||
|
if ( err instanceof CodedError ) {
|
||||||
|
return err;
|
||||||
|
} else {
|
||||||
|
return new CodedError( ErrorCode.UnexpectedError );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ export interface AdjustmentMethod {
|
|||||||
* of this method does not have PWS support, this parameter may be ignored and coordinates may be used instead.
|
* of this method does not have PWS support, this parameter may be ignored and coordinates may be used instead.
|
||||||
* @return A Promise that will be resolved with the result of the calculation, or rejected with an error message if
|
* @return A Promise that will be resolved with the result of the calculation, or rejected with an error message if
|
||||||
* the watering scale cannot be calculated.
|
* the watering scale cannot be calculated.
|
||||||
* @throws An error message can be thrown if an error occurs while calculating the watering scale.
|
* @throws A CodedError may be thrown if an error occurs while calculating the watering scale.
|
||||||
*/
|
*/
|
||||||
calculateWateringScale(
|
calculateWateringScale(
|
||||||
adjustmentOptions: AdjustmentOptions,
|
adjustmentOptions: AdjustmentOptions,
|
||||||
@@ -43,15 +43,6 @@ export interface AdjustmentMethodResponse {
|
|||||||
* watering.
|
* watering.
|
||||||
*/
|
*/
|
||||||
rainDelay?: number;
|
rainDelay?: number;
|
||||||
// TODO consider removing this field and breaking backwards compatibility to handle all errors consistently.
|
|
||||||
/**
|
|
||||||
* An message to send to the OS firmware to indicate that an error occurred while calculating the watering
|
|
||||||
* scale and the returned scale either defaulted to some reasonable value or was calculated with incomplete data.
|
|
||||||
* Older firmware versions will ignore this field (they will silently swallow the error and use the returned scale),
|
|
||||||
* but newer firmware versions may be able to alert the user that an error occurred and/or default to a
|
|
||||||
* user-configured watering scale instead of using the one returned by the AdjustmentMethod.
|
|
||||||
*/
|
|
||||||
errorMessage?: string;
|
|
||||||
/** The data that was used to calculate the watering scale, or undefined if no data was used. */
|
/** The data that was used to calculate the watering scale, or undefined if no data was used. */
|
||||||
wateringData: BaseWateringData;
|
wateringData: BaseWateringData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as moment from "moment";
|
|||||||
import { AdjustmentMethod, AdjustmentMethodResponse, AdjustmentOptions } from "./AdjustmentMethod";
|
import { AdjustmentMethod, AdjustmentMethodResponse, AdjustmentOptions } from "./AdjustmentMethod";
|
||||||
import { BaseWateringData, GeoCoordinates, PWS } from "../../types";
|
import { BaseWateringData, GeoCoordinates, PWS } from "../../types";
|
||||||
import { WeatherProvider } from "../weatherProviders/WeatherProvider";
|
import { WeatherProvider } from "../weatherProviders/WeatherProvider";
|
||||||
|
import { CodedError, ErrorCode } from "../../errors";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,7 +18,7 @@ async function calculateEToWateringScale(
|
|||||||
): Promise< AdjustmentMethodResponse > {
|
): Promise< AdjustmentMethodResponse > {
|
||||||
|
|
||||||
if ( pws ) {
|
if ( pws ) {
|
||||||
throw "ETo adjustment method does not support personal weather stations through WUnderground.";
|
throw new CodedError( ErrorCode.PwsNotSupported );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily disabled since OWM forecast data is checking if rain is forecasted for 3 hours in the future.
|
// Temporarily disabled since OWM forecast data is checking if rain is forecasted for 3 hours in the future.
|
||||||
@@ -30,7 +31,7 @@ async function calculateEToWateringScale(
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// This will throw an error message if ETo data cannot be retrieved.
|
// This will throw a CodedError if ETo data cannot be retrieved.
|
||||||
const etoData: EToData = await weatherProvider.getEToData( coordinates );
|
const etoData: EToData = await weatherProvider.getEToData( coordinates );
|
||||||
|
|
||||||
let baseETo: number;
|
let baseETo: number;
|
||||||
@@ -40,7 +41,7 @@ async function calculateEToWateringScale(
|
|||||||
if ( adjustmentOptions && "baseETo" in adjustmentOptions ) {
|
if ( adjustmentOptions && "baseETo" in adjustmentOptions ) {
|
||||||
baseETo = adjustmentOptions.baseETo
|
baseETo = adjustmentOptions.baseETo
|
||||||
} else {
|
} else {
|
||||||
throw "A baseline potential ETo must be provided.";
|
throw new CodedError( ErrorCode.MissingAdjustmentOption );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( adjustmentOptions && "elevation" in adjustmentOptions ) {
|
if ( adjustmentOptions && "elevation" in adjustmentOptions ) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AdjustmentMethod, AdjustmentMethodResponse, AdjustmentOptions } from ".
|
|||||||
import { GeoCoordinates, PWS, ZimmermanWateringData } from "../../types";
|
import { GeoCoordinates, PWS, ZimmermanWateringData } from "../../types";
|
||||||
import { validateValues } from "../weather";
|
import { validateValues } from "../weather";
|
||||||
import { WeatherProvider } from "../weatherProviders/WeatherProvider";
|
import { WeatherProvider } from "../weatherProviders/WeatherProvider";
|
||||||
|
import { CodedError, ErrorCode } from "../../errors";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,12 +39,7 @@ async function calculateZimmermanWateringScale(
|
|||||||
// 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" ], wateringData ) ) {
|
if ( !validateValues( [ "temp", "humidity", "precip" ], wateringData ) ) {
|
||||||
// Default to a scale of 100% if fields are missing.
|
// Default to a scale of 100% if fields are missing.
|
||||||
return {
|
throw new CodedError( ErrorCode.MissingWeatherField );
|
||||||
scale: 100,
|
|
||||||
rawData: rawData,
|
|
||||||
errorMessage: "Necessary field(s) were missing from ZimmermanWateringData.",
|
|
||||||
wateringData: wateringData
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let humidityBase = 30, tempBase = 70, precipBase = 0;
|
let humidityBase = 30, tempBase = 70, precipBase = 0;
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import ManualAdjustmentMethod from "./adjustmentMethods/ManualAdjustmentMethod";
|
|||||||
import ZimmermanAdjustmentMethod from "./adjustmentMethods/ZimmermanAdjustmentMethod";
|
import ZimmermanAdjustmentMethod from "./adjustmentMethods/ZimmermanAdjustmentMethod";
|
||||||
import RainDelayAdjustmentMethod from "./adjustmentMethods/RainDelayAdjustmentMethod";
|
import RainDelayAdjustmentMethod from "./adjustmentMethods/RainDelayAdjustmentMethod";
|
||||||
import EToAdjustmentMethod from "./adjustmentMethods/EToAdjustmentMethod";
|
import EToAdjustmentMethod from "./adjustmentMethods/EToAdjustmentMethod";
|
||||||
|
import { CodedError, ErrorCode, makeCodedError } from "../errors";
|
||||||
|
|
||||||
const WEATHER_PROVIDER: WeatherProvider = new ( require("./weatherProviders/" + ( process.env.WEATHER_PROVIDER || "OWM" ) ).default )();
|
const WEATHER_PROVIDER: WeatherProvider = new ( require("./weatherProviders/" + ( process.env.WEATHER_PROVIDER || "OWM" ) ).default )();
|
||||||
const PWS_WEATHER_PROVIDER: WeatherProvider = new ( require("./weatherProviders/" + ( process.env.PWS_WEATHER_PROVIDER || "WUnderground" ) ).default )();
|
const PWS_WEATHER_PROVIDER: WeatherProvider = new ( require("./weatherProviders/" + ( process.env.PWS_WEATHER_PROVIDER || "WUnderground" ) ).default )();
|
||||||
|
|
||||||
@@ -39,16 +41,16 @@ const cache = new WateringScaleCache();
|
|||||||
* Resolves a location description to geographic coordinates.
|
* Resolves a location description to geographic coordinates.
|
||||||
* @param location A partial zip/city/country or a coordinate pair.
|
* @param location A partial zip/city/country or a coordinate pair.
|
||||||
* @return A promise that will be resolved with the coordinates of the best match for the specified location, or
|
* @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.
|
* rejected with a CodedError if unable to resolve the location.
|
||||||
*/
|
*/
|
||||||
export async function resolveCoordinates( location: string ): Promise< GeoCoordinates > {
|
export async function resolveCoordinates( location: string ): Promise< GeoCoordinates > {
|
||||||
|
|
||||||
if ( !location ) {
|
if ( !location ) {
|
||||||
throw "No location specified";
|
throw new CodedError( ErrorCode.InvalidLocationFormat );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( filters.pws.test( location ) ) {
|
if ( filters.pws.test( location ) ) {
|
||||||
throw "PWS ID must be specified in the pws parameter.";
|
throw new CodedError( ErrorCode.InvalidLocationFormat );
|
||||||
} else if ( filters.gps.test( location ) ) {
|
} else if ( filters.gps.test( location ) ) {
|
||||||
const split: string[] = location.split( "," );
|
const split: string[] = location.split( "," );
|
||||||
return [ parseFloat( split[ 0 ] ), parseFloat( split[ 1 ] ) ];
|
return [ parseFloat( split[ 0 ] ), parseFloat( split[ 1 ] ) ];
|
||||||
@@ -62,7 +64,7 @@ export async function resolveCoordinates( location: string ): Promise< GeoCoordi
|
|||||||
data = await httpJSONRequest( url );
|
data = await httpJSONRequest( url );
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If the request fails, indicate no data was found.
|
// If the request fails, indicate no data was found.
|
||||||
throw "An API error occurred while attempting to resolve location";
|
throw new CodedError( ErrorCode.LocationServiceApiError );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the data is valid
|
// Check if the data is valid
|
||||||
@@ -73,7 +75,7 @@ export async function resolveCoordinates( location: string ): Promise< GeoCoordi
|
|||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Otherwise, indicate no data was found
|
// Otherwise, indicate no data was found
|
||||||
throw "No match found for specified location";
|
throw new CodedError( ErrorCode.NoLocationFound );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,7 +196,7 @@ export const getWateringData = async function( req: express.Request, res: expres
|
|||||||
remoteAddress = remoteAddress.split( "," )[ 0 ];
|
remoteAddress = remoteAddress.split( "," )[ 0 ];
|
||||||
|
|
||||||
if ( !adjustmentMethod ) {
|
if ( !adjustmentMethod ) {
|
||||||
res.send( "Error: Unknown AdjustmentMethod ID" );
|
sendWateringError( res, new CodedError( ErrorCode.InvalidAdjustmentMethod ));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,9 +209,8 @@ export const getWateringData = async function( req: express.Request, res: expres
|
|||||||
// Reconstruct JSON string from deformed controller output
|
// Reconstruct JSON string from deformed controller output
|
||||||
adjustmentOptions = JSON.parse( "{" + adjustmentOptionsString + "}" );
|
adjustmentOptions = JSON.parse( "{" + adjustmentOptionsString + "}" );
|
||||||
} catch ( err ) {
|
} catch ( err ) {
|
||||||
|
// If the JSON is not valid then abort the calculation
|
||||||
// If the JSON is not valid then abort the claculation
|
sendWateringError( res, new CodedError( ErrorCode.MalformedAdjustmentOptions ) );
|
||||||
res.send(`Error: Unable to parse options (${err})`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,8 +218,8 @@ export const getWateringData = async function( req: express.Request, res: expres
|
|||||||
let coordinates: GeoCoordinates;
|
let coordinates: GeoCoordinates;
|
||||||
try {
|
try {
|
||||||
coordinates = await resolveCoordinates( location );
|
coordinates = await resolveCoordinates( location );
|
||||||
} catch (err) {
|
} catch ( err ) {
|
||||||
res.send(`Error: Unable to resolve location (${err})`);
|
sendWateringError( res, makeCodedError( err ) );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,11 +236,11 @@ export const getWateringData = async function( req: express.Request, res: expres
|
|||||||
|
|
||||||
// Make sure that the PWS ID and API key look valid.
|
// Make sure that the PWS ID and API key look valid.
|
||||||
if ( !pwsId ) {
|
if ( !pwsId ) {
|
||||||
res.send("Error: PWS ID does not appear to be valid.");
|
sendWateringError( res, new CodedError( ErrorCode.InvalidPwsId ) );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ( !apiKey ) {
|
if ( !apiKey ) {
|
||||||
res.send("Error: PWS API key does not appear to be valid.");
|
sendWateringError( res, new CodedError( ErrorCode.InvalidPwsApiKey ) );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +257,7 @@ export const getWateringData = async function( req: express.Request, res: expres
|
|||||||
sunset: timeData.sunset,
|
sunset: timeData.sunset,
|
||||||
eip: ipToInt( remoteAddress ),
|
eip: ipToInt( remoteAddress ),
|
||||||
rawData: undefined,
|
rawData: undefined,
|
||||||
error: undefined
|
errCode: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
let cachedScale: CachedScale;
|
let cachedScale: CachedScale;
|
||||||
@@ -277,22 +278,11 @@ export const getWateringData = async function( req: express.Request, res: expres
|
|||||||
adjustmentOptions, coordinates, weatherProvider, pws
|
adjustmentOptions, coordinates, weatherProvider, pws
|
||||||
);
|
);
|
||||||
} catch ( err ) {
|
} catch ( err ) {
|
||||||
if ( typeof err != "string" ) {
|
sendWateringError( res, makeCodedError( err ) );
|
||||||
/* If an error occurs under expected circumstances (e.g. required optional fields from a weather API are
|
|
||||||
missing), an AdjustmentOption must throw a string. If a non-string error is caught, it is likely an Error
|
|
||||||
thrown by the JS engine due to unexpected circumstances. The user should not be shown the error message
|
|
||||||
since it may contain sensitive information. */
|
|
||||||
res.send( "Error: an unexpected error occurred." );
|
|
||||||
console.error( `An unexpected error occurred for ${ req.url }: `, err );
|
|
||||||
} else {
|
|
||||||
res.send( "Error: " + err );
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.scale = adjustmentMethodResponse.scale;
|
data.scale = adjustmentMethodResponse.scale;
|
||||||
data.error = adjustmentMethodResponse.errorMessage;
|
|
||||||
data.rd = adjustmentMethodResponse.rainDelay;
|
data.rd = adjustmentMethodResponse.rainDelay;
|
||||||
data.rawData = adjustmentMethodResponse.rawData;
|
data.rawData = adjustmentMethodResponse.rawData;
|
||||||
|
|
||||||
@@ -303,7 +293,7 @@ export const getWateringData = async function( req: express.Request, res: expres
|
|||||||
try {
|
try {
|
||||||
wateringData = await weatherProvider.getWateringData( coordinates );
|
wateringData = await weatherProvider.getWateringData( coordinates );
|
||||||
} catch ( err ) {
|
} catch ( err ) {
|
||||||
res.send( "Error: " + err );
|
sendWateringError( res, makeCodedError( err ) );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,7 +305,7 @@ export const getWateringData = async function( req: express.Request, res: expres
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache the watering scale if caching is enabled and no error occurred.
|
// Cache the watering scale if caching is enabled and no error occurred.
|
||||||
if ( weatherProvider.shouldCacheWateringScale() && !data.error ) {
|
if ( weatherProvider.shouldCacheWateringScale() ) {
|
||||||
cache.storeWateringScale( req.params[ 0 ], coordinates, pws, adjustmentOptions, {
|
cache.storeWateringScale( req.params[ 0 ], coordinates, pws, adjustmentOptions, {
|
||||||
scale: data.scale,
|
scale: data.scale,
|
||||||
rawData: data.rawData,
|
rawData: data.rawData,
|
||||||
@@ -324,8 +314,31 @@ export const getWateringData = async function( req: express.Request, res: expres
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the response to the client in the requested format
|
sendWateringData( res, data, outputFormat === "json" );
|
||||||
if ( outputFormat === "json" ) {
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a response to a watering scale request with an error code and a default watering scale of 100%.
|
||||||
|
* @param res The Express Response object to send the response through.
|
||||||
|
* @param error The error code to send in the response body.
|
||||||
|
* @param useJson Indicates if the response body should use a JSON format instead of a format similar to URL query strings.
|
||||||
|
*/
|
||||||
|
function sendWateringError( res: express.Response, error: CodedError, useJson: boolean = false ) {
|
||||||
|
if ( error.errCode === ErrorCode.UnexpectedError ) {
|
||||||
|
console.error( `An unexpected error occurred:`, error );
|
||||||
|
}
|
||||||
|
|
||||||
|
sendWateringData( res, { errCode: error.errCode, scale: 100 } );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a response to an HTTP request with a 200 status code.
|
||||||
|
* @param res The Express Response object to send the response through.
|
||||||
|
* @param data An object containing key/value pairs that should be formatted in the response body.
|
||||||
|
* @param useJson Indicates if the response body should use a JSON format instead of a format similar to URL query strings.
|
||||||
|
*/
|
||||||
|
function sendWateringData( res: express.Response, data: object, useJson: boolean = false ) {
|
||||||
|
if ( useJson ) {
|
||||||
res.json( data );
|
res.json( data );
|
||||||
} else {
|
} else {
|
||||||
// Return the data formatted as a URL query string.
|
// Return the data formatted as a URL query string.
|
||||||
@@ -356,7 +369,7 @@ export const getWateringData = async function( req: express.Request, res: expres
|
|||||||
}
|
}
|
||||||
res.send( formatted );
|
res.send( formatted );
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes an HTTP/HTTPS GET request to the specified URL and returns the response body.
|
* Makes an HTTP/HTTPS GET request to the specified URL and returns the response body.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { GeoCoordinates, WeatherData, ZimmermanWateringData } from "../../types"
|
|||||||
import { httpJSONRequest } from "../weather";
|
import { httpJSONRequest } from "../weather";
|
||||||
import { WeatherProvider } from "./WeatherProvider";
|
import { WeatherProvider } from "./WeatherProvider";
|
||||||
import { approximateSolarRadiation, CloudCoverInfo, EToData } from "../adjustmentMethods/EToAdjustmentMethod";
|
import { approximateSolarRadiation, CloudCoverInfo, EToData } from "../adjustmentMethods/EToAdjustmentMethod";
|
||||||
|
import { CodedError, ErrorCode } from "../../errors";
|
||||||
|
|
||||||
export default class DarkSkyWeatherProvider extends WeatherProvider {
|
export default class DarkSkyWeatherProvider extends WeatherProvider {
|
||||||
|
|
||||||
@@ -28,11 +29,11 @@ export default class DarkSkyWeatherProvider extends WeatherProvider {
|
|||||||
yesterdayData = await httpJSONRequest( yesterdayUrl );
|
yesterdayData = await httpJSONRequest( yesterdayUrl );
|
||||||
} catch ( err ) {
|
} catch ( err ) {
|
||||||
console.error( "Error retrieving weather information from Dark Sky:", err );
|
console.error( "Error retrieving weather information from Dark Sky:", err );
|
||||||
throw "An error occurred while retrieving weather information from Dark Sky."
|
throw new CodedError( ErrorCode.WeatherApiError );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( !yesterdayData.hourly || !yesterdayData.hourly.data ) {
|
if ( !yesterdayData.hourly || !yesterdayData.hourly.data ) {
|
||||||
throw "Necessary field(s) were missing from weather information returned by Dark Sky.";
|
throw new CodedError( ErrorCode.MissingWeatherField );
|
||||||
}
|
}
|
||||||
|
|
||||||
const samples = [
|
const samples = [
|
||||||
@@ -41,7 +42,7 @@ export default class DarkSkyWeatherProvider extends WeatherProvider {
|
|||||||
|
|
||||||
// Fail if not enough data is available.
|
// Fail if not enough data is available.
|
||||||
if ( samples.length !== 24 ) {
|
if ( samples.length !== 24 ) {
|
||||||
throw "Insufficient data was returned by Dark Sky.";
|
throw new CodedError( ErrorCode.InsufficientWeatherData );
|
||||||
}
|
}
|
||||||
|
|
||||||
const totals = { temp: 0, humidity: 0, precip: 0 };
|
const totals = { temp: 0, humidity: 0, precip: 0 };
|
||||||
@@ -122,7 +123,7 @@ export default class DarkSkyWeatherProvider extends WeatherProvider {
|
|||||||
try {
|
try {
|
||||||
historicData = await httpJSONRequest( historicUrl );
|
historicData = await httpJSONRequest( historicUrl );
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw "An error occurred while retrieving weather information from Dark Sky."
|
throw new CodedError( ErrorCode.WeatherApiError );
|
||||||
}
|
}
|
||||||
|
|
||||||
const cloudCoverInfo: CloudCoverInfo[] = historicData.hourly.data.map( ( hour ): CloudCoverInfo => {
|
const cloudCoverInfo: CloudCoverInfo[] = historicData.hourly.data.map( ( hour ): CloudCoverInfo => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { httpJSONRequest } from "../weather";
|
|||||||
import { WeatherProvider } from "./WeatherProvider";
|
import { WeatherProvider } from "./WeatherProvider";
|
||||||
import { approximateSolarRadiation, CloudCoverInfo, EToData } from "../adjustmentMethods/EToAdjustmentMethod";
|
import { approximateSolarRadiation, CloudCoverInfo, EToData } from "../adjustmentMethods/EToAdjustmentMethod";
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
|
import { CodedError, ErrorCode } from "../../errors";
|
||||||
|
|
||||||
export default class OWMWeatherProvider extends WeatherProvider {
|
export default class OWMWeatherProvider extends WeatherProvider {
|
||||||
|
|
||||||
@@ -25,12 +26,12 @@ export default class OWMWeatherProvider extends WeatherProvider {
|
|||||||
forecast = await httpJSONRequest( forecastUrl );
|
forecast = await httpJSONRequest( forecastUrl );
|
||||||
} catch ( err ) {
|
} catch ( err ) {
|
||||||
console.error( "Error retrieving weather information from OWM:", err );
|
console.error( "Error retrieving weather information from OWM:", err );
|
||||||
throw "An error occurred while retrieving weather information from OWM."
|
throw new CodedError( ErrorCode.WeatherApiError );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Indicate watering data could not be retrieved if the forecast data is incomplete.
|
// Indicate watering data could not be retrieved if the forecast data is incomplete.
|
||||||
if ( !forecast || !forecast.list ) {
|
if ( !forecast || !forecast.list ) {
|
||||||
throw "Necessary field(s) were missing from weather information returned by OWM.";
|
throw new CodedError( ErrorCode.MissingWeatherField );
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalTemp = 0,
|
let totalTemp = 0,
|
||||||
@@ -111,12 +112,12 @@ export default class OWMWeatherProvider extends WeatherProvider {
|
|||||||
forecast = await httpJSONRequest( forecastUrl );
|
forecast = await httpJSONRequest( forecastUrl );
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error( "Error retrieving ETo information from OWM:", err );
|
console.error( "Error retrieving ETo information from OWM:", err );
|
||||||
throw "An error occurred while retrieving ETo information from OWM."
|
throw new CodedError( ErrorCode.WeatherApiError );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Indicate ETo data could not be retrieved if the forecast data is incomplete.
|
// Indicate ETo data could not be retrieved if the forecast data is incomplete.
|
||||||
if ( !forecast || !forecast.list || forecast.list.length < 8 ) {
|
if ( !forecast || !forecast.list || forecast.list.length < 8 ) {
|
||||||
throw "Insufficient data available from OWM."
|
throw new CodedError( ErrorCode.InsufficientWeatherData );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take a sample over 24 hours.
|
// Take a sample over 24 hours.
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { GeoCoordinates, PWS, WeatherData, ZimmermanWateringData } from "../../types";
|
import { GeoCoordinates, PWS, WeatherData, ZimmermanWateringData } from "../../types";
|
||||||
import { WeatherProvider } from "./WeatherProvider";
|
import { WeatherProvider } from "./WeatherProvider";
|
||||||
import { httpJSONRequest } from "../weather";
|
import { httpJSONRequest } from "../weather";
|
||||||
|
import { CodedError, ErrorCode } from "../../errors";
|
||||||
|
|
||||||
export default class WUnderground extends WeatherProvider {
|
export default class WUnderground extends WeatherProvider {
|
||||||
|
|
||||||
async getWateringData( coordinates: GeoCoordinates, pws?: PWS ): Promise< ZimmermanWateringData > {
|
async getWateringData( coordinates: GeoCoordinates, pws?: PWS ): Promise< ZimmermanWateringData > {
|
||||||
if ( !pws ) {
|
if ( !pws ) {
|
||||||
throw "WUnderground WeatherProvider requires a PWS to be specified.";
|
throw new CodedError( ErrorCode.NoPwsProvided );
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `https://api.weather.com/v2/pws/observations/hourly/7day?stationId=${ pws.id }&format=json&units=e&apiKey=${ pws.apiKey }`;
|
const url = `https://api.weather.com/v2/pws/observations/hourly/7day?stationId=${ pws.id }&format=json&units=e&apiKey=${ pws.apiKey }`;
|
||||||
@@ -15,7 +16,7 @@ export default class WUnderground extends WeatherProvider {
|
|||||||
data = await httpJSONRequest( url );
|
data = await httpJSONRequest( url );
|
||||||
} catch ( err ) {
|
} catch ( err ) {
|
||||||
console.error( "Error retrieving weather information from WUnderground:", err );
|
console.error( "Error retrieving weather information from WUnderground:", err );
|
||||||
throw "An error occurred while retrieving weather information from WUnderground."
|
throw new CodedError( ErrorCode.WeatherApiError );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take the 24 most recent observations.
|
// Take the 24 most recent observations.
|
||||||
@@ -23,7 +24,7 @@ export default class WUnderground extends WeatherProvider {
|
|||||||
|
|
||||||
// Fail if not enough data is available.
|
// Fail if not enough data is available.
|
||||||
if ( samples.length !== 24 ) {
|
if ( samples.length !== 24 ) {
|
||||||
throw "Insufficient data was returned by WUnderground.";
|
throw new CodedError( ErrorCode.InsufficientWeatherData );
|
||||||
}
|
}
|
||||||
|
|
||||||
const totals = { temp: 0, humidity: 0, precip: 0 };
|
const totals = { temp: 0, humidity: 0, precip: 0 };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { GeoCoordinates, PWS, WeatherData, ZimmermanWateringData } from "../../types";
|
import { GeoCoordinates, PWS, WeatherData, ZimmermanWateringData } from "../../types";
|
||||||
import { EToData } from "../adjustmentMethods/EToAdjustmentMethod";
|
import { EToData } from "../adjustmentMethods/EToAdjustmentMethod";
|
||||||
|
import { CodedError, ErrorCode } from "../../errors";
|
||||||
|
|
||||||
export class WeatherProvider {
|
export class WeatherProvider {
|
||||||
/**
|
/**
|
||||||
@@ -8,11 +9,11 @@ export class WeatherProvider {
|
|||||||
* @param pws The PWS to retrieve the weather from, or undefined if a PWS should not be used. If the implementation
|
* @param pws The PWS to retrieve the weather from, or undefined if a PWS should not be used. If the implementation
|
||||||
* of this method does not have PWS support, this parameter may be ignored and coordinates may be used instead.
|
* of this method does not have PWS support, this parameter may be ignored and coordinates may be used instead.
|
||||||
* @return A Promise that will be resolved with the ZimmermanWateringData if it is successfully retrieved,
|
* @return A Promise that will be resolved with the ZimmermanWateringData if it is successfully retrieved,
|
||||||
* or rejected with an error message if an error occurs while retrieving the ZimmermanWateringData or the WeatherProvider
|
* or rejected with a CodedError if an error occurs while retrieving the ZimmermanWateringData (or the WeatherProvider
|
||||||
* does not support this method.
|
* does not support this method).
|
||||||
*/
|
*/
|
||||||
getWateringData( coordinates: GeoCoordinates, pws?: PWS ): Promise< ZimmermanWateringData > {
|
getWateringData( coordinates: GeoCoordinates, pws?: PWS ): Promise< ZimmermanWateringData > {
|
||||||
throw "Selected WeatherProvider does not support getWateringData";
|
throw new CodedError( ErrorCode.UnsupportedAdjustmentMethod );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,12 +30,11 @@ export class WeatherProvider {
|
|||||||
/**
|
/**
|
||||||
* Retrieves the data necessary for calculating potential ETo.
|
* Retrieves the data necessary for calculating potential ETo.
|
||||||
* @param coordinates The coordinates to retrieve the data for.
|
* @param coordinates The coordinates to retrieve the data for.
|
||||||
* @return A Promise that will be resolved with the EToData if it is successfully retrieved,
|
* @return A Promise that will be resolved with the EToData if it is successfully retrieved, or rejected with a
|
||||||
* or rejected with an error message if an error occurs while retrieving the EToData or the WeatherProvider does
|
* CodedError if an error occurs while retrieving the EToData (or the WeatherProvider does not support this method).
|
||||||
* not support this method.
|
|
||||||
*/
|
*/
|
||||||
getEToData( coordinates: GeoCoordinates ): Promise< EToData > {
|
getEToData( coordinates: GeoCoordinates ): Promise< EToData > {
|
||||||
throw "Selected WeatherProvider does not support getEToData";
|
throw new CodedError( ErrorCode.UnsupportedAdjustmentMethod );
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"tz": 32,
|
"tz": 32,
|
||||||
"sunrise": 332,
|
"sunrise": 332,
|
||||||
"sunset": 1203,
|
"sunset": 1203,
|
||||||
"eip": 2130706433
|
"eip": 2130706433,
|
||||||
|
"errCode": 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"adjustment1": {
|
"adjustment1": {
|
||||||
@@ -19,7 +20,8 @@
|
|||||||
"p": 1.09,
|
"p": 1.09,
|
||||||
"t": 70.8,
|
"t": 70.8,
|
||||||
"raining": 1
|
"raining": 1
|
||||||
}
|
},
|
||||||
|
"errCode": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
"errors.ts",
|
||||||
"server.ts",
|
"server.ts",
|
||||||
"types.ts",
|
"types.ts",
|
||||||
"routes/**/*"
|
"routes/**/*"
|
||||||
|
|||||||
Reference in New Issue
Block a user