diff --git a/routes/adjustmentMethods/AdjustmentMethod.ts b/routes/adjustmentMethods/AdjustmentMethod.ts index 66c1c3a..209c0e7 100644 --- a/routes/adjustmentMethods/AdjustmentMethod.ts +++ b/routes/adjustmentMethods/AdjustmentMethod.ts @@ -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 {} diff --git a/routes/adjustmentMethods/ManualAdjustmentMethod.ts b/routes/adjustmentMethods/ManualAdjustmentMethod.ts index d584f83..7c9e8f7 100644 --- a/routes/adjustmentMethods/ManualAdjustmentMethod.ts +++ b/routes/adjustmentMethods/ManualAdjustmentMethod.ts @@ -6,7 +6,8 @@ import { AdjustmentMethod, AdjustmentMethodResponse } from "./AdjustmentMethod"; */ async function calculateManualWateringScale( ): Promise< AdjustmentMethodResponse > { return { - scale: undefined + scale: undefined, + wateringData: undefined } } diff --git a/routes/adjustmentMethods/RainDelayAdjustmentMethod.ts b/routes/adjustmentMethods/RainDelayAdjustmentMethod.ts index 4b6a0e0..bfa1348 100644 --- a/routes/adjustmentMethods/RainDelayAdjustmentMethod.ts +++ b/routes/adjustmentMethods/RainDelayAdjustmentMethod.ts @@ -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 } } diff --git a/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts b/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts index c5ed019..ec4a8ae 100644 --- a/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts +++ b/routes/adjustmentMethods/ZimmermanAdjustmentMethod.ts @@ -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 } } diff --git a/routes/weather.spec.ts b/routes/weather.spec.ts index 36e92e5..c566a70 100644 --- a/routes/weather.spec.ts +++ b/routes/weather.spec.ts @@ -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 } diff --git a/routes/weather.ts b/routes/weather.ts index 1401c77..544fe6f 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -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; diff --git a/routes/weatherProviders/DarkSky.ts b/routes/weatherProviders/DarkSky.ts index 7098f39..4ff6926 100644 --- a/routes/weatherProviders/DarkSky.ts +++ b/routes/weatherProviders/DarkSky.ts @@ -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 { diff --git a/routes/weatherProviders/OWM.ts b/routes/weatherProviders/OWM.ts index fdb4023..b35b4b8 100644 --- a/routes/weatherProviders/OWM.ts +++ b/routes/weatherProviders/OWM.ts @@ -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 ]; diff --git a/routes/weatherProviders/WeatherProvider.ts b/routes/weatherProviders/WeatherProvider.ts index 4826df2..79782f3 100644 --- a/routes/weatherProviders/WeatherProvider.ts +++ b/routes/weatherProviders/WeatherProvider.ts @@ -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"; } diff --git a/routes/weatherProviders/local.ts b/routes/weatherProviders/local.ts index 71e4798..1a12eca 100644 --- a/routes/weatherProviders/local.ts +++ b/routes/weatherProviders/local.ts @@ -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 diff --git a/types.ts b/types.ts index 8dbe9f5..cf66b0f 100644 --- a/types.ts +++ b/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; }