Merge 'dev' into 'eto'

This commit is contained in:
Matthew Oslan
2019-07-06 10:15:22 -04:00
9 changed files with 250 additions and 124 deletions

69
WateringScaleCache.ts Normal file
View File

@@ -0,0 +1,69 @@
import * as NodeCache from "node-cache";
import { GeoCoordinates, PWS } from "./types";
import { AdjustmentOptions } from "./routes/adjustmentMethods/AdjustmentMethod";
import * as moment from "moment-timezone";
import * as geoTZ from "geo-tz";
import { Moment } from "moment-timezone/moment-timezone";
export default class WateringScaleCache {
private readonly cache: NodeCache = new NodeCache();
/**
* Stores the results of a watering scale calculation. The scale will be cached until the end of the day in the local
* timezone of the specified coordinates. If a scale has already been cached for the specified calculation parameters,
* this method will have no effect.
* @param adjustmentMethodId The ID of the AdjustmentMethod used to calculate this watering scale. This value should
* have the appropriate bits set for any restrictions that were used.
* @param coordinates The coordinates the watering scale was calculated for.
* @param pws The PWS used to calculate the watering scale, or undefined if one was not used.
* @param adjustmentOptions Any user-specified adjustment options that were used when calculating the watering scale.
* @param wateringScale The results of the watering scale calculation.
*/
public storeWateringScale(
adjustmentMethodId: number,
coordinates: GeoCoordinates,
pws: PWS,
adjustmentOptions: AdjustmentOptions,
wateringScale: CachedScale
): void {
// The end of the day in the controller's timezone.
const expirationDate: Moment = moment().tz( geoTZ( coordinates[ 0 ], coordinates[ 1 ] )[ 0 ] ).endOf( "day" );
const ttl: number = ( expirationDate.unix() - moment().unix() );
const key = this.makeKey( adjustmentMethodId, coordinates, pws, adjustmentOptions );
this.cache.set( key, wateringScale, ttl );
}
/**
* Retrieves a cached scale that was previously calculated with the given parameters.
* @param adjustmentMethodId The ID of the AdjustmentMethod used to calculate this watering scale. This value should
* have the appropriate bits set for any restrictions that were used.
* @param coordinates The coordinates the watering scale was calculated for.
* @param pws The PWS used to calculate the watering scale, or undefined if one was not used.
* @param adjustmentOptions Any user-specified adjustment options that were used when calculating the watering scale.
* @return The cached result of the watering scale calculation, or undefined if no values were cached.
*/
public getWateringScale(
adjustmentMethodId: number,
coordinates: GeoCoordinates,
pws: PWS,
adjustmentOptions: AdjustmentOptions
): CachedScale | undefined {
const key = this.makeKey( adjustmentMethodId, coordinates, pws, adjustmentOptions );
return this.cache.get( key );
}
private makeKey(
adjustmentMethodId: number,
coordinates: GeoCoordinates,
pws: PWS,
adjustmentOptions: AdjustmentOptions
): string {
return `${ adjustmentMethodId }#${ coordinates.join( "," ) }#${ pws ? pws.id : "" }#${ JSON.stringify( adjustmentOptions ) }`
}
}
export interface CachedScale {
scale: number;
rawData: object;
rainDelay: number;
}

69
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "os-weather-service", "name": "os-weather-service",
"version": "1.0.2", "version": "1.0.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -127,6 +127,15 @@
"integrity": "sha512-Fvm24+u85lGmV4hT5G++aht2C5I4Z4dYlWZIh62FAfFO/TfzXtPpoLI6I7AuBWkIFqZCnhFOoTT7RjjaIL5Fjg==", "integrity": "sha512-Fvm24+u85lGmV4hT5G++aht2C5I4Z4dYlWZIh62FAfFO/TfzXtPpoLI6I7AuBWkIFqZCnhFOoTT7RjjaIL5Fjg==",
"dev": true "dev": true
}, },
"@types/node-cache": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@types/node-cache/-/node-cache-4.1.3.tgz",
"integrity": "sha512-3hsqnv3H1zkOhjygJaJUYmgz5+FcPO3vejBX7cE9/cnuINOJYrzkfOnUCvpwGe9kMZANIHJA7J5pOdeyv52OEw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/range-parser": { "@types/range-parser": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
@@ -571,6 +580,11 @@
"integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=",
"dev": true "dev": true
}, },
"clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18="
},
"collection-visit": { "collection-visit": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@@ -1126,8 +1140,7 @@
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@@ -1148,14 +1161,12 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -1170,20 +1181,17 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@@ -1300,8 +1308,7 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@@ -1313,7 +1320,6 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@@ -1328,7 +1334,6 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@@ -1336,14 +1341,12 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.3.5", "version": "2.3.5",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@@ -1362,7 +1365,6 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@@ -1443,8 +1445,7 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@@ -1456,7 +1457,6 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@@ -1542,8 +1542,7 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@@ -1579,7 +1578,6 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@@ -1599,7 +1597,6 @@
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@@ -1643,14 +1640,12 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.3", "version": "3.0.3",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
} }
} }
}, },
@@ -2091,8 +2086,7 @@
"lodash": { "lodash": {
"version": "4.17.11", "version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
"dev": true
}, },
"lowercase-keys": { "lowercase-keys": {
"version": "1.0.1", "version": "1.0.1",
@@ -2465,6 +2459,15 @@
} }
} }
}, },
"node-cache": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/node-cache/-/node-cache-4.2.0.tgz",
"integrity": "sha512-obRu6/f7S024ysheAjoYFEEBqqDWv4LOMNJEuO8vMeEw2AT4z+NCzO4hlc2lhI4vATzbCQv6kke9FVdx0RbCOw==",
"requires": {
"clone": "2.x",
"lodash": "4.x"
}
},
"node-watch": { "node-watch": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.6.2.tgz", "resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.6.2.tgz",

View File

@@ -19,6 +19,7 @@
"geo-tz": "^5.0.4", "geo-tz": "^5.0.4",
"mockdate": "^2.0.2", "mockdate": "^2.0.2",
"moment-timezone": "^0.5.25", "moment-timezone": "^0.5.25",
"node-cache": "^4.2.0",
"suncalc": "^1.8.0" "suncalc": "^1.8.0"
}, },
"devDependencies": { "devDependencies": {
@@ -30,6 +31,7 @@
"@types/mocha": "^5.2.6", "@types/mocha": "^5.2.6",
"@types/moment-timezone": "^0.5.12", "@types/moment-timezone": "^0.5.12",
"@types/node": "^10.14.6", "@types/node": "^10.14.6",
"@types/node-cache": "^4.1.3",
"@types/suncalc": "^1.8.0", "@types/suncalc": "^1.8.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"mocha": "^5.2.0", "mocha": "^5.2.0",

View File

@@ -56,4 +56,9 @@ export interface AdjustmentMethodResponse {
wateringData: BaseWateringData; wateringData: BaseWateringData;
} }
export interface AdjustmentOptions {} export interface AdjustmentOptions {
/** The ID of the PWS to use, prefixed with "pws:". */
pws?: string;
/** The API key to use to access PWS data. */
key?: string;
}

View File

@@ -4,6 +4,10 @@ import * as MockExpressRequest from 'mock-express-request';
import * as MockExpressResponse from 'mock-express-response'; import * as MockExpressResponse from 'mock-express-response';
import * as MockDate from 'mockdate'; import * as MockDate from 'mockdate';
// The tests don't use OWM, but the WeatherProvider API key must be set to prevent an error from being thrown on startup.
process.env.WEATHER_PROVIDER = "OWM";
process.env.OWM_API_KEY = "NO_KEY";
import { getWateringData } from './weather'; import { getWateringData } from './weather';
import { GeoCoordinates, WeatherData, ZimmermanWateringData } from "../types"; import { GeoCoordinates, WeatherData, ZimmermanWateringData } from "../types";
import { WeatherProvider } from "./weatherProviders/WeatherProvider"; import { WeatherProvider } from "./weatherProviders/WeatherProvider";

View File

@@ -8,6 +8,7 @@ import * as geoTZ from "geo-tz";
import { BaseWateringData, GeoCoordinates, PWS, TimeData, WeatherData } from "../types"; import { BaseWateringData, GeoCoordinates, PWS, TimeData, WeatherData } from "../types";
import { WeatherProvider } from "./weatherProviders/WeatherProvider"; import { WeatherProvider } from "./weatherProviders/WeatherProvider";
import { AdjustmentMethod, AdjustmentMethodResponse, AdjustmentOptions } from "./adjustmentMethods/AdjustmentMethod"; import { AdjustmentMethod, AdjustmentMethodResponse, AdjustmentOptions } from "./adjustmentMethods/AdjustmentMethod";
import WateringScaleCache, { CachedScale } from "../WateringScaleCache";
import ManualAdjustmentMethod from "./adjustmentMethods/ManualAdjustmentMethod"; 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";
@@ -32,6 +33,8 @@ const ADJUSTMENT_METHOD: { [ key: number ] : AdjustmentMethod } = {
3: EToAdjustmentMethod 3: EToAdjustmentMethod
}; };
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.
@@ -184,7 +187,6 @@ export const getWateringData = async function( req: express.Request, res: expres
location: string | GeoCoordinates = getParameter(req.query.loc), location: string | GeoCoordinates = getParameter(req.query.loc),
outputFormat: string = getParameter(req.query.format), outputFormat: string = getParameter(req.query.format),
remoteAddress: string = getParameter(req.headers[ "x-forwarded-for" ]) || req.connection.remoteAddress, remoteAddress: string = getParameter(req.headers[ "x-forwarded-for" ]) || req.connection.remoteAddress,
pwsString: string = getParameter( req.query.pws ),
adjustmentOptions: AdjustmentOptions; adjustmentOptions: AdjustmentOptions;
// X-Forwarded-For header may contain more than one IP address and therefore // X-Forwarded-For header may contain more than one IP address and therefore
@@ -224,67 +226,108 @@ export const getWateringData = async function( req: express.Request, res: expres
// Parse the PWS information. // Parse the PWS information.
let pws: PWS | undefined = undefined; let pws: PWS | undefined = undefined;
if ( pwsString ) { if ( adjustmentOptions.pws ) {
try { if ( !adjustmentOptions.key ) {
pws = parsePWS( pwsString ); res.send("Error: An API key must be provided when using a PWS.");
} catch ( err ) {
res.send( `Error: ${ err }` );
return; return;
} }
const idMatch = adjustmentOptions.pws.match( /^pws:([a-zA-Z\d]+)$/ );
const pwsId = idMatch ? idMatch[ 1 ] : undefined;
const keyMatch = adjustmentOptions.key.match( /^[a-f\d]{32}$/ );
const apiKey = keyMatch ? keyMatch[ 0 ] : undefined;
// Make sure that the PWS ID and API key look valid.
if ( !pwsId ) {
res.send("Error: PWS ID does not appear to be valid.");
return;
}
if ( !apiKey ) {
res.send("Error: PWS API key does not appear to be valid.");
return;
}
pws = { id: pwsId, apiKey: apiKey };
} }
const weatherProvider = pws ? PWS_WEATHER_PROVIDER : WEATHER_PROVIDER; const weatherProvider = pws ? PWS_WEATHER_PROVIDER : WEATHER_PROVIDER;
let adjustmentMethodResponse: AdjustmentMethodResponse;
try {
adjustmentMethodResponse = await adjustmentMethod.calculateWateringScale(
adjustmentOptions, coordinates, weatherProvider, pws
);
} catch ( err ) {
if ( typeof err != "string" ) {
/* 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;
}
let scale = adjustmentMethodResponse.scale;
if ( checkRestrictions ) {
let wateringData: BaseWateringData = adjustmentMethodResponse.wateringData;
// Fetch the watering data if the AdjustmentMethod didn't fetch it and restrictions are being checked.
if ( checkRestrictions && !wateringData ) {
try {
wateringData = await weatherProvider.getWateringData( coordinates );
} catch ( err ) {
res.send( "Error: " + err );
return;
}
}
// Check for any user-set restrictions and change the scale to 0 if the criteria is met
if ( checkWeatherRestriction( req.params[ 0 ], wateringData ) ) {
scale = 0;
}
}
const data = { const data = {
scale: scale, scale: undefined,
rd: adjustmentMethodResponse.rainDelay, rd: undefined,
tz: getTimezone( timeData.timezone, undefined ), tz: getTimezone( timeData.timezone, undefined ),
sunrise: timeData.sunrise, sunrise: timeData.sunrise,
sunset: timeData.sunset, sunset: timeData.sunset,
eip: ipToInt( remoteAddress ), eip: ipToInt( remoteAddress ),
rawData: adjustmentMethodResponse.rawData, rawData: undefined,
error: adjustmentMethodResponse.errorMessage error: undefined
}; };
let cachedScale: CachedScale;
if ( weatherProvider.shouldCacheWateringScale() ) {
cachedScale = cache.getWateringScale( req.params[ 0 ], coordinates, pws, adjustmentOptions );
}
if ( cachedScale ) {
// Use the cached data if it exists.
data.scale = cachedScale.scale;
data.rawData = cachedScale.rawData;
data.rd = cachedScale.rainDelay;
} else {
// Calculate the watering scale if it wasn't found in the cache.
let adjustmentMethodResponse: AdjustmentMethodResponse;
try {
adjustmentMethodResponse = await adjustmentMethod.calculateWateringScale(
adjustmentOptions, coordinates, weatherProvider, pws
);
} catch ( err ) {
if ( typeof err != "string" ) {
/* 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;
}
data.scale = adjustmentMethodResponse.scale;
data.error = adjustmentMethodResponse.errorMessage;
data.rd = adjustmentMethodResponse.rainDelay;
data.rawData = adjustmentMethodResponse.rawData;
if ( checkRestrictions ) {
let wateringData: BaseWateringData = adjustmentMethodResponse.wateringData;
// Fetch the watering data if the AdjustmentMethod didn't fetch it and restrictions are being checked.
if ( checkRestrictions && !wateringData ) {
try {
wateringData = await weatherProvider.getWateringData( coordinates );
} catch ( err ) {
res.send( "Error: " + err );
return;
}
}
// Check for any user-set restrictions and change the scale to 0 if the criteria is met
if ( checkWeatherRestriction( req.params[ 0 ], wateringData ) ) {
data.scale = 0;
}
}
// Cache the watering scale if caching is enabled and no error occurred.
if ( weatherProvider.shouldCacheWateringScale() && !data.error ) {
cache.storeWateringScale( req.params[ 0 ], coordinates, pws, adjustmentOptions, {
scale: data.scale,
rawData: data.rawData,
rainDelay: data.rd
} );
}
}
// Return the response to the client in the requested format // Return the response to the client in the requested format
if ( outputFormat === "json" ) { if ( outputFormat === "json" ) {
res.json( data ); res.json( data );
@@ -452,21 +495,3 @@ export function getParameter( parameter: string | string[] ): string {
// Return an empty string if the parameter is undefined. // Return an empty string if the parameter is undefined.
return parameter || ""; return parameter || "";
} }
/**
* Creates a PWS object from a string.
* @param pwsString Information about the PWS in the format "pws:API_KEY@PWS_ID".
* @return The PWS specified by the string.
* @throws Throws an error message if the string is in an invalid format and cannot be parsed.
*/
function parsePWS( pwsString: string): PWS {
const match = pwsString.match( /^pws:([a-f\d]{32})@([a-zA-Z\d]+)$/ );
if ( !match ) {
throw "Invalid PWS format.";
}
return {
apiKey: match[ 1 ],
id: match[ 2 ]
};
}

View File

@@ -7,38 +7,36 @@ import { approximateSolarRadiation, CloudCoverInfo, EToData } from "../adjustmen
export default class DarkSkyWeatherProvider extends WeatherProvider { export default class DarkSkyWeatherProvider extends WeatherProvider {
private readonly API_KEY: string;
public constructor() {
super();
this.API_KEY = process.env.DARKSKY_API_KEY;
if (!this.API_KEY) {
throw "DARKSKY_API_KEY environment variable is not defined.";
}
}
public async getWateringData( coordinates: GeoCoordinates ): Promise< ZimmermanWateringData > { public async getWateringData( coordinates: GeoCoordinates ): Promise< ZimmermanWateringData > {
// The Unix timestamp of 24 hours ago. // The Unix timestamp of 24 hours ago.
const yesterdayTimestamp: number = moment().subtract( 1, "day" ).unix(); const yesterdayTimestamp: number = moment().subtract( 1, "day" ).unix();
const todayTimestamp: number = moment().unix();
const DARKSKY_API_KEY = process.env.DARKSKY_API_KEY, const yesterdayUrl = `https://api.darksky.net/forecast/${ this.API_KEY }/${ coordinates[ 0 ] },${ coordinates[ 1 ] },${ yesterdayTimestamp }?exclude=currently,minutely,daily,alerts,flags`;
yesterdayUrl = `https://api.darksky.net/forecast/${ DARKSKY_API_KEY }/${ coordinates[ 0 ] },${ coordinates[ 1 ] },${ yesterdayTimestamp }?exclude=currently,minutely,daily,alerts,flags`,
todayUrl = `https://api.darksky.net/forecast/${ DARKSKY_API_KEY }/${ coordinates[ 0 ] },${ coordinates[ 1 ] },${ todayTimestamp }?exclude=currently,minutely,daily,alerts,flags`;
let yesterdayData, todayData; let yesterdayData;
try { try {
yesterdayData = await httpJSONRequest( yesterdayUrl ); yesterdayData = await httpJSONRequest( yesterdayUrl );
todayData = await httpJSONRequest( todayUrl );
} 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 "An error occurred while retrieving weather information from Dark Sky."
} }
if ( !todayData.hourly || !todayData.hourly.data || !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 "Necessary field(s) were missing from weather information returned by Dark Sky.";
} }
/* The number of hourly forecasts to use from today's data. This will only include elements that contain historic
data (not forecast data). */
// Find the first element that contains forecast data.
const todayElements = Math.min( 24, todayData.hourly.data.findIndex( ( data ) => data.time > todayTimestamp - 60 * 60 ) );
/* Take as much data as possible from the first elements of today's data and take the remaining required data from
the remaining data from the last elements of yesterday's data. */
const samples = [ const samples = [
...yesterdayData.hourly.data.slice( todayElements - 24 ), ...yesterdayData.hourly.data
...todayData.hourly.data.slice( 0, todayElements )
]; ];
// Fail if not enough data is available. // Fail if not enough data is available.
@@ -70,8 +68,7 @@ export default class DarkSkyWeatherProvider extends WeatherProvider {
} }
public async getWeatherData( coordinates: GeoCoordinates ): Promise< WeatherData > { public async getWeatherData( coordinates: GeoCoordinates ): Promise< WeatherData > {
const DARKSKY_API_KEY = process.env.DARKSKY_API_KEY, const forecastUrl = `https://api.darksky.net/forecast/${ this.API_KEY }/${ coordinates[ 0 ] },${ coordinates[ 1 ] }?exclude=minutely,alerts,flags`;
forecastUrl = `https://api.darksky.net/forecast/${ DARKSKY_API_KEY }/${ coordinates[ 0 ] },${ coordinates[ 1 ] }?exclude=minutely,alerts,flags`;
let forecast; let forecast;
try { try {
@@ -163,4 +160,8 @@ export default class DarkSkyWeatherProvider extends WeatherProvider {
precip: ( historicData.daily.data[ 0 ].precipIntensity || 0 ) * 24 precip: ( historicData.daily.data[ 0 ].precipIntensity || 0 ) * 24
}; };
} }
public shouldCacheWateringScale(): boolean {
return true;
}
} }

View File

@@ -6,9 +6,18 @@ import * as moment from "moment";
export default class OWMWeatherProvider extends WeatherProvider { export default class OWMWeatherProvider extends WeatherProvider {
private readonly API_KEY: string;
public constructor() {
super();
this.API_KEY = process.env.OWM_API_KEY;
if (!this.API_KEY) {
throw "OWM_API_KEY environment variable is not defined.";
}
}
public async getWateringData( coordinates: GeoCoordinates ): Promise< ZimmermanWateringData > { public async getWateringData( coordinates: GeoCoordinates ): Promise< ZimmermanWateringData > {
const OWM_API_KEY = process.env.OWM_API_KEY, const forecastUrl = `http://api.openweathermap.org/data/2.5/forecast?appid=${ this.API_KEY }&units=imperial&lat=${ coordinates[ 0 ] }&lon=${ coordinates[ 1 ] }`;
forecastUrl = "http://api.openweathermap.org/data/2.5/forecast?appid=" + OWM_API_KEY + "&units=imperial&lat=" + coordinates[ 0 ] + "&lon=" + coordinates[ 1 ];
// Perform the HTTP request to retrieve the weather data // Perform the HTTP request to retrieve the weather data
let forecast; let forecast;
@@ -45,9 +54,8 @@ export default class OWMWeatherProvider extends WeatherProvider {
} }
public async getWeatherData( coordinates: GeoCoordinates ): Promise< WeatherData > { public async getWeatherData( coordinates: GeoCoordinates ): Promise< WeatherData > {
const OWM_API_KEY = process.env.OWM_API_KEY, const currentUrl = `http://api.openweathermap.org/data/2.5/weather?appid=${ this.API_KEY }&units=imperial&lat=${ coordinates[ 0 ] }&lon=${ coordinates[ 1 ] }`,
currentUrl = "http://api.openweathermap.org/data/2.5/weather?appid=" + OWM_API_KEY + "&units=imperial&lat=" + coordinates[ 0 ] + "&lon=" + coordinates[ 1 ], forecastDailyUrl = `http://api.openweathermap.org/data/2.5/forecast/daily?appid=${ this.API_KEY }&units=imperial&lat=${ coordinates[ 0 ] }&lon=${ coordinates[ 1 ] }`;
forecastDailyUrl = "http://api.openweathermap.org/data/2.5/forecast/daily?appid=" + OWM_API_KEY + "&units=imperial&lat=" + coordinates[ 0 ] + "&lon=" + coordinates[ 1 ];
let current, forecast; let current, forecast;
try { try {

View File

@@ -36,4 +36,13 @@ export class WeatherProvider {
getEToData( coordinates: GeoCoordinates ): Promise< EToData > { getEToData( coordinates: GeoCoordinates ): Promise< EToData > {
throw "Selected WeatherProvider does not support getEToData"; throw "Selected WeatherProvider does not support getEToData";
}; };
/**
* Returns a boolean indicating if watering scales calculated using data from this WeatherProvider should be cached
* until the end of the day in timezone the data was for.
* @return a boolean indicating if watering scales calculated using data from this WeatherProvider should be cached.
*/
shouldCacheWateringScale(): boolean {
return false;
}
} }