Cache calculated watering scale when using data from Dark Sky
This commit is contained in:
69
WateringScaleCache.ts
Normal file
69
WateringScaleCache.ts
Normal 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
69
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "os-weather-service",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -127,6 +127,15 @@
|
||||
"integrity": "sha512-Fvm24+u85lGmV4hT5G++aht2C5I4Z4dYlWZIh62FAfFO/TfzXtPpoLI6I7AuBWkIFqZCnhFOoTT7RjjaIL5Fjg==",
|
||||
"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": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
|
||||
@@ -571,6 +580,11 @@
|
||||
"integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=",
|
||||
"dev": true
|
||||
},
|
||||
"clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18="
|
||||
},
|
||||
"collection-visit": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
|
||||
@@ -1126,8 +1140,7 @@
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@@ -1148,14 +1161,12 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -1170,20 +1181,17 @@
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@@ -1300,8 +1308,7 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@@ -1313,7 +1320,6 @@
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -1328,7 +1334,6 @@
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@@ -1336,14 +1341,12 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@@ -1362,7 +1365,6 @@
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@@ -1443,8 +1445,7 @@
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -1456,7 +1457,6 @@
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -1542,8 +1542,7 @@
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@@ -1579,7 +1578,6 @@
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -1599,7 +1597,6 @@
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@@ -1643,14 +1640,12 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2091,8 +2086,7 @@
|
||||
"lodash": {
|
||||
"version": "4.17.11",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
|
||||
},
|
||||
"lowercase-keys": {
|
||||
"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": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.6.2.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"geo-tz": "^5.0.4",
|
||||
"mockdate": "^2.0.2",
|
||||
"moment-timezone": "^0.5.25",
|
||||
"node-cache": "^4.2.0",
|
||||
"suncalc": "^1.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -30,6 +31,7 @@
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/moment-timezone": "^0.5.12",
|
||||
"@types/node": "^10.14.6",
|
||||
"@types/node-cache": "^4.1.3",
|
||||
"@types/suncalc": "^1.8.0",
|
||||
"chai": "^4.2.0",
|
||||
"mocha": "^5.2.0",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { AdjustmentMethod, AdjustmentMethodResponse, AdjustmentOptions } from ".
|
||||
import ManualAdjustmentMethod from "./adjustmentMethods/ManualAdjustmentMethod";
|
||||
import ZimmermanAdjustmentMethod from "./adjustmentMethods/ZimmermanAdjustmentMethod";
|
||||
import RainDelayAdjustmentMethod from "./adjustmentMethods/RainDelayAdjustmentMethod";
|
||||
import WateringScaleCache, { CachedScale } from "../WateringScaleCache";
|
||||
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 )();
|
||||
|
||||
@@ -30,6 +31,8 @@ const ADJUSTMENT_METHOD: { [ key: number ] : AdjustmentMethod } = {
|
||||
2: RainDelayAdjustmentMethod
|
||||
};
|
||||
|
||||
const cache = new WateringScaleCache();
|
||||
|
||||
/**
|
||||
* Resolves a location description to geographic coordinates.
|
||||
* @param location A partial zip/city/country or a coordinate pair.
|
||||
@@ -227,57 +230,83 @@ export const getWateringData = async function( req: express.Request, res: expres
|
||||
}
|
||||
|
||||
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 = {
|
||||
scale: scale,
|
||||
rd: adjustmentMethodResponse.rainDelay,
|
||||
scale: undefined,
|
||||
rd: undefined,
|
||||
tz: getTimezone( timeData.timezone, undefined ),
|
||||
sunrise: timeData.sunrise,
|
||||
sunset: timeData.sunset,
|
||||
eip: ipToInt( remoteAddress ),
|
||||
rawData: adjustmentMethodResponse.rawData,
|
||||
error: adjustmentMethodResponse.errorMessage
|
||||
rawData: undefined,
|
||||
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
|
||||
if ( outputFormat === "json" ) {
|
||||
res.json( data );
|
||||
|
||||
@@ -111,4 +111,8 @@ export default class DarkSkyWeatherProvider extends WeatherProvider {
|
||||
|
||||
return weather;
|
||||
}
|
||||
|
||||
public shouldCacheWateringScale(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,4 +24,13 @@ export class WeatherProvider {
|
||||
getWeatherData( coordinates : GeoCoordinates ): Promise< WeatherData > {
|
||||
throw "Selected WeatherProvider does not support getWeatherData";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user