Refactor watering scale logic flow
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { GeoCoordinates, WateringData } from "../../types";
|
||||
import { BaseWateringData, GeoCoordinates } from "../../types";
|
||||
import { WeatherProvider } from "../weatherProviders/WeatherProvider";
|
||||
|
||||
|
||||
@@ -7,10 +7,8 @@ export interface AdjustmentMethod {
|
||||
* Calculates the percentage that should be used to scale watering time.
|
||||
* @param adjustmentOptions The user-specified options for the calculation. No checks will be made to ensure the
|
||||
* AdjustmentOptions are the correct type that the function 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 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.
|
||||
* @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.
|
||||
@@ -18,7 +16,6 @@ export interface AdjustmentMethod {
|
||||
*/
|
||||
calculateWateringScale(
|
||||
adjustmentOptions: AdjustmentOptions,
|
||||
wateringData: WateringData | undefined,
|
||||
coordinates: GeoCoordinates,
|
||||
weatherProvider: WeatherProvider
|
||||
): Promise< AdjustmentMethodResponse >;
|
||||
@@ -52,6 +49,8 @@ export interface AdjustmentMethodResponse {
|
||||
* user-configured watering scale instead of using the one returned by the AdjustmentMethod.
|
||||
*/
|
||||
errorMessage?: string;
|
||||
/** The data that was used to calculate the watering scale, or undefined if no data was used. */
|
||||
wateringData: BaseWateringData;
|
||||
}
|
||||
|
||||
export interface AdjustmentOptions {}
|
||||
|
||||
@@ -6,7 +6,8 @@ import { AdjustmentMethod, AdjustmentMethodResponse } from "./AdjustmentMethod";
|
||||
*/
|
||||
async function calculateManualWateringScale( ): Promise< AdjustmentMethodResponse > {
|
||||
return {
|
||||
scale: undefined
|
||||
scale: undefined,
|
||||
wateringData: undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
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.
|
||||
*/
|
||||
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 d = adjustmentOptions.hasOwnProperty( "d" ) ? adjustmentOptions.d : 24;
|
||||
return {
|
||||
scale: undefined,
|
||||
rawData: { raining: raining ? 1 : 0 },
|
||||
rainDelay: raining ? d : undefined
|
||||
rainDelay: raining ? d : undefined,
|
||||
wateringData: wateringData
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { AdjustmentMethod, AdjustmentMethodResponse, AdjustmentOptions } from "./AdjustmentMethod";
|
||||
import { WateringData } from "../../types";
|
||||
import { GeoCoordinates, ZimmermanWateringData } from "../../types";
|
||||
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.
|
||||
* (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 ): Promise< AdjustmentMethodResponse > {
|
||||
const wateringData: ZimmermanWateringData = await weatherProvider.getWateringData( coordinates );
|
||||
|
||||
// Temporarily disabled since OWM forecast data is checking if rain is forecasted for 3 hours in the future.
|
||||
/*
|
||||
@@ -15,7 +17,8 @@ async function calculateZimmermanWateringScale( adjustmentOptions: ZimmermanAdju
|
||||
if ( wateringData && wateringData.raining ) {
|
||||
return {
|
||||
scale: 0,
|
||||
rawData: { raining: 1 }
|
||||
rawData: { raining: 1 },
|
||||
wateringData: wateringData
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -33,7 +36,8 @@ async function calculateZimmermanWateringScale( adjustmentOptions: ZimmermanAdju
|
||||
return {
|
||||
scale: 100,
|
||||
rawData: rawData,
|
||||
errorMessage: "Necessary field(s) were missing from WateringData."
|
||||
errorMessage: "Necessary field(s) were missing from ZimmermanWateringData.",
|
||||
wateringData: wateringData
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,7 +68,8 @@ async function calculateZimmermanWateringScale( adjustmentOptions: ZimmermanAdju
|
||||
return {
|
||||
// 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 ) ),
|
||||
rawData: rawData
|
||||
rawData: rawData,
|
||||
wateringData: wateringData
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as MockExpressResponse from 'mock-express-response';
|
||||
import * as MockDate from 'mockdate';
|
||||
|
||||
import { getWateringData } from './weather';
|
||||
import { GeoCoordinates, WateringData, WeatherData } from "../types";
|
||||
import { GeoCoordinates, ZimmermanWateringData, WeatherData } from "../types";
|
||||
import { WeatherProvider } from "./weatherProviders/WeatherProvider";
|
||||
|
||||
const expected = require( '../test/expected.json' );
|
||||
@@ -77,7 +77,7 @@ export class MockWeatherProvider extends WeatherProvider {
|
||||
this.mockData = mockData;
|
||||
}
|
||||
|
||||
public async getWateringData( coordinates: GeoCoordinates ): Promise< WateringData > {
|
||||
public async getWateringData( coordinates: GeoCoordinates ): Promise< ZimmermanWateringData > {
|
||||
const data = this.mockData.wateringData;
|
||||
if ( !data.weatherProvider ) {
|
||||
data.weatherProvider = "mock";
|
||||
@@ -97,6 +97,6 @@ export class MockWeatherProvider extends WeatherProvider {
|
||||
}
|
||||
|
||||
interface MockWeatherData {
|
||||
wateringData?: WateringData,
|
||||
wateringData?: ZimmermanWateringData,
|
||||
weatherData?: WeatherData
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as SunCalc from "suncalc";
|
||||
import * as moment from "moment-timezone";
|
||||
import * as geoTZ from "geo-tz";
|
||||
|
||||
import { GeoCoordinates, TimeData, WateringData, WeatherData } from "../types";
|
||||
import { GeoCoordinates, TimeData, WeatherData, BaseWateringData } from "../types";
|
||||
import { WeatherProvider } from "./weatherProviders/WeatherProvider";
|
||||
import { AdjustmentMethod, AdjustmentMethodResponse, AdjustmentOptions } from "./adjustmentMethods/AdjustmentMethod";
|
||||
import ManualAdjustmentMethod from "./adjustmentMethods/ManualAdjustmentMethod";
|
||||
@@ -121,7 +121,7 @@ function getTimeData( coordinates: GeoCoordinates ): TimeData {
|
||||
* @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.
|
||||
*/
|
||||
function checkWeatherRestriction( adjustmentValue: number, weather: WateringData ): boolean {
|
||||
function checkWeatherRestriction( adjustmentValue: number, weather: BaseWateringData ): boolean {
|
||||
|
||||
const californiaRestriction = ( adjustmentValue >> 7 ) & 1;
|
||||
|
||||
@@ -211,22 +211,12 @@ export const getWateringData = async function( req: express.Request, res: expres
|
||||
return;
|
||||
}
|
||||
|
||||
// Continue with the weather request
|
||||
let timeData: TimeData = getTimeData( coordinates );
|
||||
let wateringData: WateringData;
|
||||
if ( adjustmentMethod !== ManualAdjustmentMethod || checkRestrictions ) {
|
||||
try {
|
||||
wateringData = await weatherProvider.getWateringData( coordinates );
|
||||
} catch ( err ) {
|
||||
res.send( "Error: " + err );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let adjustmentMethodResponse: AdjustmentMethodResponse;
|
||||
try {
|
||||
adjustmentMethodResponse = await adjustmentMethod.calculateWateringScale(
|
||||
adjustmentOptions, wateringData, coordinates, weatherProvider
|
||||
adjustmentOptions, coordinates, weatherProvider
|
||||
);
|
||||
} catch ( err ) {
|
||||
if ( typeof err != "string" ) {
|
||||
@@ -244,7 +234,19 @@ export const getWateringData = async function( req: express.Request, res: expres
|
||||
}
|
||||
|
||||
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
|
||||
if ( checkWeatherRestriction( req.params[ 0 ], wateringData ) ) {
|
||||
scale = 0;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as moment from "moment-timezone";
|
||||
|
||||
import { GeoCoordinates, WateringData, WeatherData } from "../../types";
|
||||
import { GeoCoordinates, WeatherData, ZimmermanWateringData } from "../../types";
|
||||
import { httpJSONRequest } from "../weather";
|
||||
import { WeatherProvider } from "./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.
|
||||
const yesterdayTimestamp: number = moment().subtract( 1, "day" ).unix();
|
||||
const todayTimestamp: number = moment().unix();
|
||||
@@ -47,9 +47,16 @@ export default class DarkSkyWeatherProvider extends WeatherProvider {
|
||||
|
||||
const totals = { temp: 0, humidity: 0, precip: 0 };
|
||||
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.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 {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { GeoCoordinates, WateringData, WeatherData } from "../../types";
|
||||
import { GeoCoordinates, WeatherData, ZimmermanWateringData } from "../../types";
|
||||
import { httpJSONRequest } from "../weather";
|
||||
import { WeatherProvider } from "./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,
|
||||
forecastUrl = "http://api.openweathermap.org/data/2.5/forecast?appid=" + OWM_API_KEY + "&units=imperial&lat=" + coordinates[ 0 ] + "&lon=" + coordinates[ 1 ];
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { GeoCoordinates, WateringData, WeatherData } from "../../types";
|
||||
import { GeoCoordinates, ZimmermanWateringData, WeatherData } from "../../types";
|
||||
|
||||
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.
|
||||
* @return A Promise that will be resolved with the WateringData if it is successfully retrieved,
|
||||
* or rejected with an error message if an error occurs while retrieving the WateringData or the WeatherProvider
|
||||
* @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.
|
||||
*/
|
||||
getWateringData( coordinates : GeoCoordinates ): Promise< WateringData > {
|
||||
getWateringData( coordinates : GeoCoordinates ): Promise< ZimmermanWateringData > {
|
||||
throw "Selected WeatherProvider does not support getWateringData";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as express from "express";
|
||||
import { CronJob } from "cron";
|
||||
import { GeoCoordinates, WateringData } from "../../types";
|
||||
import { GeoCoordinates, ZimmermanWateringData } from "../../types";
|
||||
import { WeatherProvider } from "./WeatherProvider";
|
||||
|
||||
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 {
|
||||
|
||||
public async getWateringData( coordinates: GeoCoordinates ): Promise< WateringData > {
|
||||
const result: WateringData = {
|
||||
...yesterday as WateringData,
|
||||
public async getWateringData( coordinates: GeoCoordinates ): Promise< ZimmermanWateringData > {
|
||||
const result: ZimmermanWateringData = {
|
||||
...yesterday as ZimmermanWateringData,
|
||||
// Use today's weather if we dont have information for yesterday yet (i.e. on startup)
|
||||
...today,
|
||||
// PWS report "buckets" so consider it still raining if last bucket was less than an hour ago
|
||||
|
||||
13
types.ts
13
types.ts
@@ -50,20 +50,23 @@ export interface WeatherDataForecast {
|
||||
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
|
||||
* historic data from the past day, but may also use forecasted data for the next day if historical data is not
|
||||
* available.
|
||||
*/
|
||||
export interface WateringData {
|
||||
/** The WeatherProvider that generated this data. */
|
||||
weatherProvider: WeatherProviderId;
|
||||
export interface ZimmermanWateringData extends BaseWateringData {
|
||||
/** The average temperature over the window (in Fahrenheit). */
|
||||
temp: number;
|
||||
/** The average humidity over the window (as a percentage). */
|
||||
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. */
|
||||
raining: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user