Merge 'dev' into 'eto'
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",
|
"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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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,16 +226,55 @@ 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;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
scale: undefined,
|
||||||
|
rd: undefined,
|
||||||
|
tz: getTimezone( timeData.timezone, undefined ),
|
||||||
|
sunrise: timeData.sunrise,
|
||||||
|
sunset: timeData.sunset,
|
||||||
|
eip: ipToInt( remoteAddress ),
|
||||||
|
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;
|
let adjustmentMethodResponse: AdjustmentMethodResponse;
|
||||||
try {
|
try {
|
||||||
adjustmentMethodResponse = await adjustmentMethod.calculateWateringScale(
|
adjustmentMethodResponse = await adjustmentMethod.calculateWateringScale(
|
||||||
@@ -254,7 +295,10 @@ export const getWateringData = async function( req: express.Request, res: expres
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let scale = adjustmentMethodResponse.scale;
|
data.scale = adjustmentMethodResponse.scale;
|
||||||
|
data.error = adjustmentMethodResponse.errorMessage;
|
||||||
|
data.rd = adjustmentMethodResponse.rainDelay;
|
||||||
|
data.rawData = adjustmentMethodResponse.rawData;
|
||||||
|
|
||||||
if ( checkRestrictions ) {
|
if ( checkRestrictions ) {
|
||||||
let wateringData: BaseWateringData = adjustmentMethodResponse.wateringData;
|
let wateringData: BaseWateringData = adjustmentMethodResponse.wateringData;
|
||||||
@@ -270,20 +314,19 @@ export const getWateringData = async function( req: express.Request, res: expres
|
|||||||
|
|
||||||
// Check for any user-set restrictions and change the scale to 0 if the criteria is met
|
// Check for any user-set restrictions and change the scale to 0 if the criteria is met
|
||||||
if ( checkWeatherRestriction( req.params[ 0 ], wateringData ) ) {
|
if ( checkWeatherRestriction( req.params[ 0 ], wateringData ) ) {
|
||||||
scale = 0;
|
data.scale = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
// Cache the watering scale if caching is enabled and no error occurred.
|
||||||
scale: scale,
|
if ( weatherProvider.shouldCacheWateringScale() && !data.error ) {
|
||||||
rd: adjustmentMethodResponse.rainDelay,
|
cache.storeWateringScale( req.params[ 0 ], coordinates, pws, adjustmentOptions, {
|
||||||
tz: getTimezone( timeData.timezone, undefined ),
|
scale: data.scale,
|
||||||
sunrise: timeData.sunrise,
|
rawData: data.rawData,
|
||||||
sunset: timeData.sunset,
|
rainDelay: data.rd
|
||||||
eip: ipToInt( remoteAddress ),
|
} );
|
||||||
rawData: adjustmentMethodResponse.rawData,
|
}
|
||||||
error: adjustmentMethodResponse.errorMessage
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// 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" ) {
|
||||||
@@ -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 ]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user