Merge pull request #81 from Derpthemeus/add-error-codes

Add error codes to watering data errors
This commit is contained in:
Samer Albahra
2019-09-09 17:33:29 -07:00
committed by GitHub
11 changed files with 163 additions and 71 deletions

85
errors.ts Normal file
View 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 );
}
}

View File

@@ -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;
} }

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 );
} }
// 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 ) {

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 { 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;

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 );
} }
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.
@@ -344,7 +357,7 @@ export const getWateringData = async function( req: express.Request, res: expres
case "object": case "object":
// Convert objects to JSON. // Convert objects to JSON.
value = JSON.stringify( value ); value = JSON.stringify( value );
// Fallthrough. // Fallthrough.
case "string": case "string":
/* URL encode strings. Since the OS firmware uses a primitive version of query string parsing and /* URL encode strings. Since the OS firmware uses a primitive version of query string parsing and
decoding, only some characters need to be escaped and only spaces ("+" or "%20") will be decoded. */ decoding, only some characters need to be escaped and only spaces ("+" or "%20") will be decoded. */
@@ -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.

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 );
} }
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 => {

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 );
} }
// 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.

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 );
} }
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 };

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 );
} }
/** /**
@@ -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 );
}; };
/** /**

View File

@@ -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
} }
} }
} }

View File

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