Add error codes to watering data errors

This commit is contained in:
Matthew Oslan
2019-08-24 19:02:41 -04:00
parent ebf78eb677
commit 9b99b993ab
10 changed files with 159 additions and 55 deletions

80
errors.ts Normal file
View File

@@ -0,0 +1,80 @@
export enum ErrorCode {
/** An error was not properly handled and assigned a more specific error code. */
UnexpectedError = 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 with a numeric code that can be used to identify the type of error. */
export class CodedError {
public readonly errCode: ErrorCode;
public readonly message: string;
public constructor( errCode: ErrorCode, message: string ) {
this.errCode = errCode;
this.message = message;
}
}
/**
* 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 {
// Include the current timestamp in the error message so the full error can easily be found in the logs.
return new CodedError( ErrorCode.UnexpectedError, "Unexpected error occurred at timestamp " + Date.now() );
}
}

View File

@@ -1,5 +1,6 @@
import { BaseWateringData, GeoCoordinates, PWS } from "../../types"; import { BaseWateringData, GeoCoordinates, PWS } from "../../types";
import { WeatherProvider } from "../weatherProviders/WeatherProvider"; import { WeatherProvider } from "../weatherProviders/WeatherProvider";
import { ErrorCode } from "../../errors";
export interface AdjustmentMethod { export interface AdjustmentMethod {
@@ -14,7 +15,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,7 +44,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 * 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. * scale and the returned scale either defaulted to some reasonable value or was calculated with incomplete data.
@@ -51,7 +51,9 @@ export interface AdjustmentMethodResponse {
* but newer firmware versions may be able to alert the user that an error occurred and/or default to a * 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. * user-configured watering scale instead of using the one returned by the AdjustmentMethod.
*/ */
errorMessage?: string; errMessage?: string;
/** A code describing the type of error that occurred (if one occurred). */
errCode?: ErrorCode;
/** 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;
} }

View File

@@ -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, "ETo adjustment method does not support personal weather stations through WUnderground." );
} }
// 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, "A baseline potential ETo must be provided." );
} }
if ( adjustmentOptions && "elevation" in adjustmentOptions ) { if ( adjustmentOptions && "elevation" in adjustmentOptions ) {

View File

@@ -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 { ErrorCode } from "../../errors";
/** /**
@@ -41,7 +42,8 @@ async function calculateZimmermanWateringScale(
return { return {
scale: 100, scale: 100,
rawData: rawData, rawData: rawData,
errorMessage: "Necessary field(s) were missing from ZimmermanWateringData.", errCode: ErrorCode.MissingWeatherField,
errMessage: "Necessary field(s) were missing from ZimmermanWateringData.",
wateringData: wateringData wateringData: wateringData
}; };
} }

View File

@@ -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, "No location specified" );
} }
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, "PWS ID must be specified in the pws adjustment option." );
} 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, "An API error occurred while attempting to resolve location" );
} }
// 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, "No match found for specified location" );
} }
} }
} }
@@ -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" ); sendWateringData( res, { errCode: ErrorCode.InvalidAdjustmentMethod, errMessage: "Invalid AdjustmentMethod ID" } );
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 sendWateringData( res, { errCode: ErrorCode.MalformedAdjustmentOptions, errMessage: `Unable to parse options (${ err })` } );
res.send(`Error: Unable to parse options (${err})`);
return; return;
} }
@@ -218,7 +219,12 @@ export const getWateringData = async function( req: express.Request, res: expres
try { try {
coordinates = await resolveCoordinates( location ); coordinates = await resolveCoordinates( location );
} catch ( err ) { } catch ( err ) {
res.send(`Error: Unable to resolve location (${err})`); let codedError: CodedError = makeCodedError( err );
if ( codedError.errCode === ErrorCode.UnexpectedError ) {
console.error( `An unexpected error occurred during location resolution for "${ req.url }": `, err );
}
sendWateringData( res, { errCode: codedError.errCode, errMessage: `Unable to resolve location "${ location }" (${ codedError.message })` } );
return; return;
} }
@@ -235,11 +241,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."); sendWateringData( res, { errCode: ErrorCode.InvalidPwsId, errMessage: "PWS ID does not appear to be valid" } );
return; return;
} }
if ( !apiKey ) { if ( !apiKey ) {
res.send("Error: PWS API key does not appear to be valid."); sendWateringData( res, { errCode: ErrorCode.InvalidPwsApiKey, errMessage: "PWS API key does not appear to be valid" } );
return; return;
} }
@@ -256,7 +262,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 errMessage: undefined
}; };
let cachedScale: CachedScale; let cachedScale: CachedScale;
@@ -277,22 +283,17 @@ 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" ) { let codedError: CodedError = makeCodedError( err );
/* If an error occurs under expected circumstances (e.g. required optional fields from a weather API are if ( codedError.errCode === ErrorCode.UnexpectedError ) {
missing), an AdjustmentOption must throw a string. If a non-string error is caught, it is likely an Error console.error( `An unexpected error occurred during watering scale calculation for "${ req.url }": `, err );
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 );
} }
sendWateringData( res, { errCode: codedError.errCode, errMessage: codedError.message } );
return; return;
} }
data.scale = adjustmentMethodResponse.scale; data.scale = adjustmentMethodResponse.scale;
data.error = adjustmentMethodResponse.errorMessage; data.errMessage = adjustmentMethodResponse.errMessage;
data.rd = adjustmentMethodResponse.rainDelay; data.rd = adjustmentMethodResponse.rainDelay;
data.rawData = adjustmentMethodResponse.rawData; data.rawData = adjustmentMethodResponse.rawData;
@@ -303,7 +304,12 @@ 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 ); let codedError: CodedError = makeCodedError( err );
if ( codedError.errCode === ErrorCode.UnexpectedError ) {
console.error( `An unexpected error occurred during restriction checks for "${ req.url }": `, err );
}
sendWateringData( res, { errCode: codedError.errCode, errMessage: codedError.message } );
return; return;
} }
} }
@@ -315,7 +321,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() && !data.errMessage ) {
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 +330,17 @@ 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 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 +371,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.

View File

@@ -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, "An error occurred while retrieving weather information from Dark Sky." );
} }
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, "Necessary field(s) were missing from weather information returned by Dark Sky." );
} }
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, "Insufficient data was returned by Dark Sky." );
} }
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, "An error occurred while retrieving weather information from Dark Sky." );
} }
const cloudCoverInfo: CloudCoverInfo[] = historicData.hourly.data.map( ( hour ): CloudCoverInfo => { const cloudCoverInfo: CloudCoverInfo[] = historicData.hourly.data.map( ( hour ): CloudCoverInfo => {

View File

@@ -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, "An error occurred while retrieving weather information from OWM." );
} }
// 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, "Necessary field(s) were missing from weather information returned by OWM." );
} }
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, "An error occurred while retrieving ETo information from OWM." );
} }
// 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, "Insufficient data available from OWM." );
} }
// Take a sample over 24 hours. // Take a sample over 24 hours.

View File

@@ -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, "WUnderground WeatherProvider requires a PWS to be specified." );
} }
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, "An error occurred while retrieving weather information from WUnderground." );
} }
// 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, "Insufficient data was returned by WUnderground." );
} }
const totals = { temp: 0, humidity: 0, precip: 0 }; const totals = { temp: 0, humidity: 0, precip: 0 };

View File

@@ -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, "Selected WeatherProvider does not support getWateringData" );
} }
/** /**
@@ -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, "Selected WeatherProvider does not support getEToData" );
}; };
/** /**

View File

@@ -8,6 +8,7 @@
"skipLibCheck": true "skipLibCheck": true
}, },
"include": [ "include": [
"errors.ts",
"server.ts", "server.ts",
"types.ts", "types.ts",
"routes/**/*" "routes/**/*"