Merge pull request #70 from Derpthemeus/add-pws-support
Add PWS support
This commit is contained in:
@@ -1,27 +1,26 @@
|
|||||||
import { GeoCoordinates, WateringData } from "../../types";
|
import { BaseWateringData, GeoCoordinates, PWS } from "../../types";
|
||||||
import { WeatherProvider } from "../weatherProviders/WeatherProvider";
|
import { WeatherProvider } from "../weatherProviders/WeatherProvider";
|
||||||
|
|
||||||
|
|
||||||
export interface AdjustmentMethod {
|
export interface AdjustmentMethod {
|
||||||
/**
|
/**
|
||||||
* Calculates the percentage that should be used to scale watering time.
|
* Calculates the percentage that should be used to scale watering time.
|
||||||
* @param adjustmentOptions The user-specified options for the calculation, or undefined/null if no custom values
|
* @param adjustmentOptions The user-specified options for the calculation. No checks will be made to ensure the
|
||||||
* are to be used. No checks will be made to ensure the AdjustmentOptions are the correct type that the function
|
* AdjustmentOptions are the correct type that the function is expecting or to ensure that any of its fields are valid.
|
||||||
* is expecting or to ensure that any of its fields are valid.
|
|
||||||
* @param wateringData The basic weather information of the watering site. This may be undefined if an error occurred
|
|
||||||
* while retrieving the data.
|
|
||||||
* @param coordinates The coordinates of the watering site.
|
* @param coordinates The coordinates of the watering site.
|
||||||
* @param weatherProvider The WeatherProvider that should be used if the adjustment method needs to obtain any more
|
* @param weatherProvider The WeatherProvider that should be used if the adjustment method needs to obtain any
|
||||||
* weather data.
|
* weather data.
|
||||||
|
* @param pws The PWS to retrieve weather data 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.
|
||||||
* @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 An error message can be thrown if an error occurs while calculating the watering scale.
|
||||||
*/
|
*/
|
||||||
calculateWateringScale(
|
calculateWateringScale(
|
||||||
adjustmentOptions: AdjustmentOptions,
|
adjustmentOptions: AdjustmentOptions,
|
||||||
wateringData: WateringData | undefined,
|
|
||||||
coordinates: GeoCoordinates,
|
coordinates: GeoCoordinates,
|
||||||
weatherProvider: WeatherProvider
|
weatherProvider: WeatherProvider,
|
||||||
|
pws?: PWS
|
||||||
): Promise< AdjustmentMethodResponse >;
|
): Promise< AdjustmentMethodResponse >;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +52,8 @@ export interface AdjustmentMethodResponse {
|
|||||||
* user-configured watering scale instead of using the one returned by the AdjustmentMethod.
|
* user-configured watering scale instead of using the one returned by the AdjustmentMethod.
|
||||||
*/
|
*/
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
|
/** The data that was used to calculate the watering scale, or undefined if no data was used. */
|
||||||
|
wateringData: BaseWateringData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdjustmentOptions {}
|
export interface AdjustmentOptions {}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { AdjustmentMethod, AdjustmentMethodResponse } from "./AdjustmentMethod";
|
|||||||
*/
|
*/
|
||||||
async function calculateManualWateringScale( ): Promise< AdjustmentMethodResponse > {
|
async function calculateManualWateringScale( ): Promise< AdjustmentMethodResponse > {
|
||||||
return {
|
return {
|
||||||
scale: undefined
|
scale: undefined,
|
||||||
|
wateringData: undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { AdjustmentMethod, AdjustmentMethodResponse, AdjustmentOptions } from "./AdjustmentMethod";
|
import { AdjustmentMethod, AdjustmentMethodResponse, AdjustmentOptions } from "./AdjustmentMethod";
|
||||||
import { WateringData } from "../../types";
|
import { GeoCoordinates, ZimmermanWateringData } from "../../types";
|
||||||
|
import { WeatherProvider } from "../weatherProviders/WeatherProvider";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only delays watering if it is currently raining and does not adjust the watering scale.
|
* Only delays watering if it is currently raining and does not adjust the watering scale.
|
||||||
*/
|
*/
|
||||||
async function calculateRainDelayWateringScale( adjustmentOptions: RainDelayAdjustmentOptions, wateringData: WateringData | undefined ): Promise< AdjustmentMethodResponse > {
|
async function calculateRainDelayWateringScale( adjustmentOptions: RainDelayAdjustmentOptions, coordinates: GeoCoordinates, weatherProvider: WeatherProvider ): Promise< AdjustmentMethodResponse > {
|
||||||
|
const wateringData: ZimmermanWateringData = await weatherProvider.getWateringData( coordinates );
|
||||||
const raining = wateringData && wateringData.raining;
|
const raining = wateringData && wateringData.raining;
|
||||||
const d = adjustmentOptions && adjustmentOptions.hasOwnProperty( "d" ) ? adjustmentOptions.d : 24;
|
const d = adjustmentOptions.hasOwnProperty( "d" ) ? adjustmentOptions.d : 24;
|
||||||
return {
|
return {
|
||||||
scale: undefined,
|
scale: undefined,
|
||||||
rawData: { raining: raining ? 1 : 0 },
|
rawData: { raining: raining ? 1 : 0 },
|
||||||
rainDelay: raining ? d : undefined
|
rainDelay: raining ? d : undefined,
|
||||||
|
wateringData: wateringData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { AdjustmentMethod, AdjustmentMethodResponse, AdjustmentOptions } from "./AdjustmentMethod";
|
import { AdjustmentMethod, AdjustmentMethodResponse, AdjustmentOptions } from "./AdjustmentMethod";
|
||||||
import { WateringData } from "../../types";
|
import { GeoCoordinates, PWS, ZimmermanWateringData } from "../../types";
|
||||||
import { validateValues } from "../weather";
|
import { validateValues } from "../weather";
|
||||||
|
import { WeatherProvider } from "../weatherProviders/WeatherProvider";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates how much watering should be scaled based on weather and adjustment options using the Zimmerman method.
|
* Calculates how much watering should be scaled based on weather and adjustment options using the Zimmerman method.
|
||||||
* (https://github.com/rszimm/sprinklers_pi/wiki/Weather-adjustments#formula-for-setting-the-scale)
|
* (https://github.com/rszimm/sprinklers_pi/wiki/Weather-adjustments#formula-for-setting-the-scale)
|
||||||
*/
|
*/
|
||||||
async function calculateZimmermanWateringScale( adjustmentOptions: ZimmermanAdjustmentOptions, wateringData: WateringData | undefined ): Promise< AdjustmentMethodResponse > {
|
async function calculateZimmermanWateringScale(
|
||||||
|
adjustmentOptions: ZimmermanAdjustmentOptions,
|
||||||
|
coordinates: GeoCoordinates,
|
||||||
|
weatherProvider: WeatherProvider,
|
||||||
|
pws?: PWS
|
||||||
|
): Promise< AdjustmentMethodResponse > {
|
||||||
|
const wateringData: ZimmermanWateringData = await weatherProvider.getWateringData( coordinates, pws );
|
||||||
|
|
||||||
// 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.
|
||||||
/*
|
/*
|
||||||
@@ -15,7 +22,8 @@ async function calculateZimmermanWateringScale( adjustmentOptions: ZimmermanAdju
|
|||||||
if ( wateringData && wateringData.raining ) {
|
if ( wateringData && wateringData.raining ) {
|
||||||
return {
|
return {
|
||||||
scale: 0,
|
scale: 0,
|
||||||
rawData: { raining: 1 }
|
rawData: { raining: 1 },
|
||||||
|
wateringData: wateringData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
@@ -33,25 +41,23 @@ async function calculateZimmermanWateringScale( adjustmentOptions: ZimmermanAdju
|
|||||||
return {
|
return {
|
||||||
scale: 100,
|
scale: 100,
|
||||||
rawData: rawData,
|
rawData: rawData,
|
||||||
errorMessage: "Necessary field(s) were missing from WateringData."
|
errorMessage: "Necessary field(s) were missing from ZimmermanWateringData.",
|
||||||
|
wateringData: wateringData
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let humidityBase = 30, tempBase = 70, precipBase = 0;
|
let humidityBase = 30, tempBase = 70, precipBase = 0;
|
||||||
|
|
||||||
// Get baseline conditions for 100% water level, if provided
|
// Get baseline conditions for 100% water level, if provided
|
||||||
if ( adjustmentOptions ) {
|
|
||||||
humidityBase = adjustmentOptions.hasOwnProperty( "bh" ) ? adjustmentOptions.bh : humidityBase;
|
humidityBase = adjustmentOptions.hasOwnProperty( "bh" ) ? adjustmentOptions.bh : humidityBase;
|
||||||
tempBase = adjustmentOptions.hasOwnProperty( "bt" ) ? adjustmentOptions.bt : tempBase;
|
tempBase = adjustmentOptions.hasOwnProperty( "bt" ) ? adjustmentOptions.bt : tempBase;
|
||||||
precipBase = adjustmentOptions.hasOwnProperty( "br" ) ? adjustmentOptions.br : precipBase;
|
precipBase = adjustmentOptions.hasOwnProperty( "br" ) ? adjustmentOptions.br : precipBase;
|
||||||
}
|
|
||||||
|
|
||||||
let humidityFactor = ( humidityBase - wateringData.humidity ),
|
let humidityFactor = ( humidityBase - wateringData.humidity ),
|
||||||
tempFactor = ( ( wateringData.temp - tempBase ) * 4 ),
|
tempFactor = ( ( wateringData.temp - tempBase ) * 4 ),
|
||||||
precipFactor = ( ( precipBase - wateringData.precip ) * 200 );
|
precipFactor = ( ( precipBase - wateringData.precip ) * 200 );
|
||||||
|
|
||||||
// Apply adjustment options, if provided, by multiplying the percentage against the factor
|
// Apply adjustment options, if provided, by multiplying the percentage against the factor
|
||||||
if ( adjustmentOptions ) {
|
|
||||||
if ( adjustmentOptions.hasOwnProperty( "h" ) ) {
|
if ( adjustmentOptions.hasOwnProperty( "h" ) ) {
|
||||||
humidityFactor = humidityFactor * ( adjustmentOptions.h / 100 );
|
humidityFactor = humidityFactor * ( adjustmentOptions.h / 100 );
|
||||||
}
|
}
|
||||||
@@ -63,12 +69,12 @@ async function calculateZimmermanWateringScale( adjustmentOptions: ZimmermanAdju
|
|||||||
if ( adjustmentOptions.hasOwnProperty( "r" ) ) {
|
if ( adjustmentOptions.hasOwnProperty( "r" ) ) {
|
||||||
precipFactor = precipFactor * ( adjustmentOptions.r / 100 );
|
precipFactor = precipFactor * ( adjustmentOptions.r / 100 );
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Apply all of the weather modifying factors and clamp the result between 0 and 200%.
|
// Apply all of the weather modifying factors and clamp the result between 0 and 200%.
|
||||||
scale: Math.floor( Math.min( Math.max( 0, 100 + humidityFactor + tempFactor + precipFactor ), 200 ) ),
|
scale: Math.floor( Math.min( Math.max( 0, 100 + humidityFactor + tempFactor + precipFactor ), 200 ) ),
|
||||||
rawData: rawData
|
rawData: rawData,
|
||||||
|
wateringData: wateringData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as MockExpressResponse from 'mock-express-response';
|
|||||||
import * as MockDate from 'mockdate';
|
import * as MockDate from 'mockdate';
|
||||||
|
|
||||||
import { getWateringData } from './weather';
|
import { getWateringData } from './weather';
|
||||||
import { GeoCoordinates, WateringData, WeatherData } from "../types";
|
import { GeoCoordinates, ZimmermanWateringData, WeatherData } from "../types";
|
||||||
import { WeatherProvider } from "./weatherProviders/WeatherProvider";
|
import { WeatherProvider } from "./weatherProviders/WeatherProvider";
|
||||||
|
|
||||||
const expected = require( '../test/expected.json' );
|
const expected = require( '../test/expected.json' );
|
||||||
@@ -77,7 +77,7 @@ export class MockWeatherProvider extends WeatherProvider {
|
|||||||
this.mockData = mockData;
|
this.mockData = mockData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getWateringData( coordinates: GeoCoordinates ): Promise< WateringData > {
|
public async getWateringData( coordinates: GeoCoordinates ): Promise< ZimmermanWateringData > {
|
||||||
const data = this.mockData.wateringData;
|
const data = this.mockData.wateringData;
|
||||||
if ( !data.weatherProvider ) {
|
if ( !data.weatherProvider ) {
|
||||||
data.weatherProvider = "mock";
|
data.weatherProvider = "mock";
|
||||||
@@ -97,6 +97,6 @@ export class MockWeatherProvider extends WeatherProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MockWeatherData {
|
interface MockWeatherData {
|
||||||
wateringData?: WateringData,
|
wateringData?: ZimmermanWateringData,
|
||||||
weatherData?: WeatherData
|
weatherData?: WeatherData
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import * as SunCalc from "suncalc";
|
|||||||
import * as moment from "moment-timezone";
|
import * as moment from "moment-timezone";
|
||||||
import * as geoTZ from "geo-tz";
|
import * as geoTZ from "geo-tz";
|
||||||
|
|
||||||
import { GeoCoordinates, TimeData, WateringData, 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 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";
|
||||||
const weatherProvider: 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 )();
|
||||||
|
|
||||||
// Define regex filters to match against location
|
// Define regex filters to match against location
|
||||||
const filters = {
|
const filters = {
|
||||||
@@ -42,7 +43,7 @@ async function resolveCoordinates( location: string ): Promise< GeoCoordinates >
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ( filters.pws.test( location ) ) {
|
if ( filters.pws.test( location ) ) {
|
||||||
throw "Weather Underground is discontinued";
|
throw "PWS ID must be specified in the pws parameter.";
|
||||||
} 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 ] ) ];
|
||||||
@@ -121,7 +122,7 @@ function getTimeData( coordinates: GeoCoordinates ): TimeData {
|
|||||||
* @param weather Watering data to use to determine if any restrictions apply.
|
* @param weather Watering data to use to determine if any restrictions apply.
|
||||||
* @return A boolean indicating if the watering level should be set to 0% due to a restriction.
|
* @return A boolean indicating if the watering level should be set to 0% due to a restriction.
|
||||||
*/
|
*/
|
||||||
function checkWeatherRestriction( adjustmentValue: number, weather: WateringData ): boolean {
|
function checkWeatherRestriction( adjustmentValue: number, weather: BaseWateringData ): boolean {
|
||||||
|
|
||||||
const californiaRestriction = ( adjustmentValue >> 7 ) & 1;
|
const californiaRestriction = ( adjustmentValue >> 7 ) & 1;
|
||||||
|
|
||||||
@@ -154,7 +155,7 @@ export const getWeatherData = async function( req: express.Request, res: express
|
|||||||
const timeData: TimeData = getTimeData( coordinates );
|
const timeData: TimeData = getTimeData( coordinates );
|
||||||
let weatherData: WeatherData;
|
let weatherData: WeatherData;
|
||||||
try {
|
try {
|
||||||
weatherData = await weatherProvider.getWeatherData( coordinates );
|
weatherData = await WEATHER_PROVIDER.getWeatherData( coordinates );
|
||||||
} catch ( err ) {
|
} catch ( err ) {
|
||||||
res.send( "Error: " + err );
|
res.send( "Error: " + err );
|
||||||
return;
|
return;
|
||||||
@@ -181,6 +182,7 @@ 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
|
||||||
@@ -211,22 +213,24 @@ export const getWateringData = async function( req: express.Request, res: expres
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue with the weather request
|
|
||||||
let timeData: TimeData = getTimeData( coordinates );
|
let timeData: TimeData = getTimeData( coordinates );
|
||||||
let wateringData: WateringData;
|
|
||||||
if ( adjustmentMethod !== ManualAdjustmentMethod || checkRestrictions ) {
|
// Parse the PWS information.
|
||||||
|
let pws: PWS | undefined = undefined;
|
||||||
|
if ( pwsString ) {
|
||||||
try {
|
try {
|
||||||
wateringData = await weatherProvider.getWateringData( coordinates );
|
pws = parsePWS( pwsString );
|
||||||
} catch ( err ) {
|
} catch ( err ) {
|
||||||
res.send( "Error: " + err );
|
res.send( `Error: ${ err }` );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const weatherProvider = pws ? PWS_WEATHER_PROVIDER : WEATHER_PROVIDER;
|
||||||
let adjustmentMethodResponse: AdjustmentMethodResponse;
|
let adjustmentMethodResponse: AdjustmentMethodResponse;
|
||||||
try {
|
try {
|
||||||
adjustmentMethodResponse = await adjustmentMethod.calculateWateringScale(
|
adjustmentMethodResponse = await adjustmentMethod.calculateWateringScale(
|
||||||
adjustmentOptions, wateringData, coordinates, weatherProvider
|
adjustmentOptions, coordinates, weatherProvider, pws
|
||||||
);
|
);
|
||||||
} catch ( err ) {
|
} catch ( err ) {
|
||||||
if ( typeof err != "string" ) {
|
if ( typeof err != "string" ) {
|
||||||
@@ -244,7 +248,19 @@ export const getWateringData = async function( req: express.Request, res: expres
|
|||||||
}
|
}
|
||||||
|
|
||||||
let scale = adjustmentMethodResponse.scale;
|
let scale = adjustmentMethodResponse.scale;
|
||||||
if ( wateringData ) {
|
|
||||||
|
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
|
// 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;
|
scale = 0;
|
||||||
@@ -429,3 +445,21 @@ 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 ]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import * as moment from "moment-timezone";
|
import * as moment from "moment-timezone";
|
||||||
|
|
||||||
import { GeoCoordinates, WateringData, WeatherData } from "../../types";
|
import { GeoCoordinates, WeatherData, ZimmermanWateringData } from "../../types";
|
||||||
import { httpJSONRequest } from "../weather";
|
import { httpJSONRequest } from "../weather";
|
||||||
import { WeatherProvider } from "./WeatherProvider";
|
import { WeatherProvider } from "./WeatherProvider";
|
||||||
|
|
||||||
export default class DarkSkyWeatherProvider extends WeatherProvider {
|
export default class DarkSkyWeatherProvider extends WeatherProvider {
|
||||||
|
|
||||||
public async getWateringData( coordinates: GeoCoordinates ): Promise< WateringData > {
|
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 todayTimestamp: number = moment().unix();
|
||||||
@@ -47,9 +47,16 @@ export default class DarkSkyWeatherProvider extends WeatherProvider {
|
|||||||
|
|
||||||
const totals = { temp: 0, humidity: 0, precip: 0 };
|
const totals = { temp: 0, humidity: 0, precip: 0 };
|
||||||
for ( const sample of samples ) {
|
for ( const sample of samples ) {
|
||||||
|
/*
|
||||||
|
* If temperature or humidity is missing from a sample, the total will become NaN. This is intended since
|
||||||
|
* calculateWateringScale will treat NaN as a missing value and temperature/humidity can't be accurately
|
||||||
|
* calculated when data is missing from some samples (since they follow diurnal cycles and will be
|
||||||
|
* significantly skewed if data is missing for several consecutive hours).
|
||||||
|
*/
|
||||||
totals.temp += sample.temperature;
|
totals.temp += sample.temperature;
|
||||||
totals.humidity += sample.humidity;
|
totals.humidity += sample.humidity;
|
||||||
totals.precip += sample.precipIntensity
|
// This field may be missing from the response if it is snowing.
|
||||||
|
totals.precip += sample.precipIntensity || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { GeoCoordinates, WateringData, WeatherData } from "../../types";
|
import { GeoCoordinates, WeatherData, ZimmermanWateringData } from "../../types";
|
||||||
import { httpJSONRequest } from "../weather";
|
import { httpJSONRequest } from "../weather";
|
||||||
import { WeatherProvider } from "./WeatherProvider";
|
import { WeatherProvider } from "./WeatherProvider";
|
||||||
|
|
||||||
export default class OWMWeatherProvider extends WeatherProvider {
|
export default class OWMWeatherProvider extends WeatherProvider {
|
||||||
|
|
||||||
public async getWateringData( coordinates: GeoCoordinates ): Promise< WateringData > {
|
public async getWateringData( coordinates: GeoCoordinates ): Promise< ZimmermanWateringData > {
|
||||||
const OWM_API_KEY = process.env.OWM_API_KEY,
|
const OWM_API_KEY = process.env.OWM_API_KEY,
|
||||||
forecastUrl = "http://api.openweathermap.org/data/2.5/forecast?appid=" + OWM_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 ];
|
||||||
|
|
||||||
|
|||||||
44
routes/weatherProviders/WUnderground.ts
Normal file
44
routes/weatherProviders/WUnderground.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { GeoCoordinates, PWS, WeatherData, ZimmermanWateringData } from "../../types";
|
||||||
|
import { WeatherProvider } from "./WeatherProvider";
|
||||||
|
import { httpJSONRequest } from "../weather";
|
||||||
|
|
||||||
|
export default class WUnderground extends WeatherProvider {
|
||||||
|
|
||||||
|
async getWateringData( coordinates: GeoCoordinates, pws?: PWS ): Promise< ZimmermanWateringData > {
|
||||||
|
if ( !pws ) {
|
||||||
|
throw "WUnderground WeatherProvider requires a PWS to be specified.";
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://api.weather.com/v2/pws/observations/hourly/7day?stationId=${ pws.id }&format=json&units=e&apiKey=${ pws.apiKey }`;
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await httpJSONRequest( url );
|
||||||
|
} catch ( err ) {
|
||||||
|
console.error( "Error retrieving weather information from WUnderground:", err );
|
||||||
|
throw "An error occurred while retrieving weather information from WUnderground."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the 24 most recent observations.
|
||||||
|
const samples = data.observations.slice( -24 );
|
||||||
|
|
||||||
|
// Fail if not enough data is available.
|
||||||
|
if ( samples.length !== 24 ) {
|
||||||
|
throw "Insufficient data was returned by WUnderground.";
|
||||||
|
}
|
||||||
|
|
||||||
|
const totals = { temp: 0, humidity: 0, precip: 0 };
|
||||||
|
for ( const sample of samples ) {
|
||||||
|
totals.temp += sample.imperial.tempAvg;
|
||||||
|
totals.humidity += sample.humidityAvg;
|
||||||
|
totals.precip += sample.imperial.precipRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
weatherProvider: "WUnderground",
|
||||||
|
temp: totals.temp / samples.length,
|
||||||
|
humidity: totals.humidity / samples.length,
|
||||||
|
precip: totals.precip,
|
||||||
|
raining: samples[ samples.length - 1 ].imperial.precipRate > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import { GeoCoordinates, WateringData, WeatherData } from "../../types";
|
import { GeoCoordinates, PWS, WeatherData, ZimmermanWateringData } from "../../types";
|
||||||
|
|
||||||
export class WeatherProvider {
|
export class WeatherProvider {
|
||||||
/**
|
/**
|
||||||
* Retrieves weather data necessary for watering level calculations.
|
* Retrieves weather data necessary for Zimmerman watering level calculations.
|
||||||
* @param coordinates The coordinates to retrieve the watering data for.
|
* @param coordinates The coordinates to retrieve the watering data for.
|
||||||
* @return A Promise that will be resolved with the WateringData if it is successfully retrieved,
|
* @param pws The PWS to retrieve the weather from, or undefined if a PWS should not be used. If the implementation
|
||||||
* or rejected with an error message if an error occurs while retrieving the WateringData or the WeatherProvider
|
* 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,
|
||||||
|
* or rejected with an error message if an error occurs while retrieving the ZimmermanWateringData or the WeatherProvider
|
||||||
* does not support this method.
|
* does not support this method.
|
||||||
*/
|
*/
|
||||||
getWateringData( coordinates : GeoCoordinates ): Promise< WateringData > {
|
getWateringData( coordinates: GeoCoordinates, pws?: PWS ): Promise< ZimmermanWateringData > {
|
||||||
throw "Selected WeatherProvider does not support getWateringData";
|
throw "Selected WeatherProvider does not support getWateringData";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as express from "express";
|
import * as express from "express";
|
||||||
import { CronJob } from "cron";
|
import { CronJob } from "cron";
|
||||||
import { GeoCoordinates, WateringData } from "../../types";
|
import { GeoCoordinates, ZimmermanWateringData } from "../../types";
|
||||||
import { WeatherProvider } from "./WeatherProvider";
|
import { WeatherProvider } from "./WeatherProvider";
|
||||||
|
|
||||||
const count = { temp: 0, humidity: 0 };
|
const count = { temp: 0, humidity: 0 };
|
||||||
@@ -46,9 +46,9 @@ export const captureWUStream = function( req: express.Request, res: express.Resp
|
|||||||
|
|
||||||
export default class LocalWeatherProvider extends WeatherProvider {
|
export default class LocalWeatherProvider extends WeatherProvider {
|
||||||
|
|
||||||
public async getWateringData( coordinates: GeoCoordinates ): Promise< WateringData > {
|
public async getWateringData( coordinates: GeoCoordinates ): Promise< ZimmermanWateringData > {
|
||||||
const result: WateringData = {
|
const result: ZimmermanWateringData = {
|
||||||
...yesterday as WateringData,
|
...yesterday as ZimmermanWateringData,
|
||||||
// Use today's weather if we dont have information for yesterday yet (i.e. on startup)
|
// Use today's weather if we dont have information for yesterday yet (i.e. on startup)
|
||||||
...today,
|
...today,
|
||||||
// PWS report "buckets" so consider it still raining if last bucket was less than an hour ago
|
// PWS report "buckets" so consider it still raining if last bucket was less than an hour ago
|
||||||
|
|||||||
18
types.ts
18
types.ts
@@ -1,6 +1,9 @@
|
|||||||
/** Geographic coordinates. The 1st element is the latitude, and the 2nd element is the longitude. */
|
/** Geographic coordinates. The 1st element is the latitude, and the 2nd element is the longitude. */
|
||||||
export type GeoCoordinates = [number, number];
|
export type GeoCoordinates = [number, number];
|
||||||
|
|
||||||
|
/** A PWS ID and API key. */
|
||||||
|
export type PWS = { id: string, apiKey: string };
|
||||||
|
|
||||||
export interface TimeData {
|
export interface TimeData {
|
||||||
/** The UTC offset, in minutes. This uses POSIX offsets, which are the negation of typically used offsets
|
/** The UTC offset, in minutes. This uses POSIX offsets, which are the negation of typically used offsets
|
||||||
* (https://github.com/eggert/tz/blob/2017b/etcetera#L36-L42).
|
* (https://github.com/eggert/tz/blob/2017b/etcetera#L36-L42).
|
||||||
@@ -50,22 +53,25 @@ export interface WeatherDataForecast {
|
|||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BaseWateringData {
|
||||||
|
/** The WeatherProvider that generated this data. */
|
||||||
|
weatherProvider: WeatherProviderId;
|
||||||
|
/** The total precipitation over the window (in inches). */
|
||||||
|
precip: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data from a 24 hour window that is used to calculate how watering levels should be scaled. This should ideally use
|
* Data from a 24 hour window that is used to calculate how watering levels should be scaled. This should ideally use
|
||||||
* historic data from the past day, but may also use forecasted data for the next day if historical data is not
|
* historic data from the past day, but may also use forecasted data for the next day if historical data is not
|
||||||
* available.
|
* available.
|
||||||
*/
|
*/
|
||||||
export interface WateringData {
|
export interface ZimmermanWateringData extends BaseWateringData {
|
||||||
/** The WeatherProvider that generated this data. */
|
|
||||||
weatherProvider: WeatherProviderId;
|
|
||||||
/** The average temperature over the window (in Fahrenheit). */
|
/** The average temperature over the window (in Fahrenheit). */
|
||||||
temp: number;
|
temp: number;
|
||||||
/** The average humidity over the window (as a percentage). */
|
/** The average humidity over the window (as a percentage). */
|
||||||
humidity: number;
|
humidity: number;
|
||||||
/** The total precipitation over the window (in inches). */
|
|
||||||
precip: number;
|
|
||||||
/** A boolean indicating if it is raining at the time that this data was retrieved. */
|
/** A boolean indicating if it is raining at the time that this data was retrieved. */
|
||||||
raining: boolean;
|
raining: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WeatherProviderId = "OWM" | "DarkSky" | "local" | "mock";
|
export type WeatherProviderId = "OWM" | "DarkSky" | "local" | "mock" | "WUnderground";
|
||||||
|
|||||||
Reference in New Issue
Block a user