Rebase on master
This commit is contained in:
20
package-lock.json
generated
20
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "os-weather-service",
|
"name": "os-weather-service",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -54,6 +54,16 @@
|
|||||||
"@types/express": "*"
|
"@types/express": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/cron": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cron/-/cron-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-LRu/XiiOExELholyEwEuSTPAEiO+sVR1nXmWEjezneGgYpDyMNVIsjiaHYBoCEUJo4F1hCOlAzQAh80iEUVbKw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"moment": ">=2.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/dotenv": {
|
"@types/dotenv": {
|
||||||
"version": "6.1.1",
|
"version": "6.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-6.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-6.1.1.tgz",
|
||||||
@@ -498,6 +508,14 @@
|
|||||||
"vary": "^1"
|
"vary": "^1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cron": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cron/-/cron-1.7.1.tgz",
|
||||||
|
"integrity": "sha512-gmMB/pJcqUVs/NklR1sCGlNYM7TizEw+1gebz20BMc/8bTm/r7QUp3ZPSPlG8Z5XRlvb7qhjEjq/+bdIfUCL2A==",
|
||||||
|
"requires": {
|
||||||
|
"moment-timezone": "^0.5.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"currently-unhandled": {
|
"currently-unhandled": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "os-weather-service",
|
"name": "os-weather-service",
|
||||||
"description": "OpenSprinkler Weather Service",
|
"description": "OpenSprinkler Weather Service",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"repository": "https://github.com/OpenSprinkler/Weather-Weather",
|
"repository": "https://github.com/OpenSprinkler/Weather-Weather",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha --exit test",
|
"test": "mocha --exit test",
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"cron": "^1.3.0",
|
||||||
"dotenv": "^6.2.0",
|
"dotenv": "^6.2.0",
|
||||||
"express": "^4.16.4",
|
"express": "^4.16.4",
|
||||||
"geo-tz": "^4.0.2",
|
"geo-tz": "^4.0.2",
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.5",
|
"@types/cors": "^2.8.5",
|
||||||
|
"@types/cron": "^1.3.0",
|
||||||
"@types/dotenv": "^6.1.1",
|
"@types/dotenv": "^6.1.1",
|
||||||
"@types/express": "^4.16.1",
|
"@types/express": "^4.16.1",
|
||||||
"@types/moment-timezone": "^0.5.12",
|
"@types/moment-timezone": "^0.5.12",
|
||||||
|
|||||||
85
routes/local.ts
Normal file
85
routes/local.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import * as express from "express";
|
||||||
|
|
||||||
|
const CronJob = require( "cron" ).CronJob,
|
||||||
|
server = require( "../server.js" ),
|
||||||
|
count = { temp: 0, humidity: 0 };
|
||||||
|
|
||||||
|
let today: PWSStatus = {},
|
||||||
|
yesterday: PWSStatus = {},
|
||||||
|
last_bucket: Date,
|
||||||
|
current_date: Date = new Date();
|
||||||
|
|
||||||
|
function sameDay(d1: Date, d2: Date): boolean {
|
||||||
|
return d1.getFullYear() === d2.getFullYear() &&
|
||||||
|
d1.getMonth() === d2.getMonth() &&
|
||||||
|
d1.getDate() === d2.getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.captureWUStream = function( req: express.Request, res: express.Response ) {
|
||||||
|
let prev: number, curr: number;
|
||||||
|
|
||||||
|
if ( !( "dateutc" in req.query ) || !sameDay( current_date, new Date( req.query.dateutc + "Z") )) {
|
||||||
|
res.send( "Error: Bad date range\n" );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ( "tempf" in req.query ) && !isNaN( curr = parseFloat( req.query.tempf ) ) && curr !== -9999.0 ) {
|
||||||
|
prev = ( "temp" in today ) ? today.temp : 0;
|
||||||
|
today.temp = ( prev * count.temp + curr ) / ( ++count.temp );
|
||||||
|
}
|
||||||
|
if ( ( "humidity" in req.query ) && !isNaN( curr = parseFloat( req.query.humidity ) ) && curr !== -9999.0 ) {
|
||||||
|
prev = ( "humidity" in today ) ? today.humidity : 0;
|
||||||
|
today.humidity = ( prev * count.humidity + curr ) / ( ++count.humidity );
|
||||||
|
}
|
||||||
|
if ( ( "dailyrainin" in req.query ) && !isNaN( curr = parseFloat( req.query.dailyrainin ) ) && curr !== -9999.0 ) {
|
||||||
|
today.precip = curr;
|
||||||
|
}
|
||||||
|
if ( ( "rainin" in req.query ) && !isNaN( curr = parseFloat( req.query.rainin ) ) && curr > 0 ) {
|
||||||
|
last_bucket = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log( "OpenSprinkler Weather Observation: %s", JSON.stringify( req.query ) );
|
||||||
|
|
||||||
|
res.send( "success\n" );
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.useLocalWeather = function(): boolean {
|
||||||
|
return server.pws !== "none" ? true : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getLocalWeather = function(): LocalWeather {
|
||||||
|
const result: LocalWeather = {};
|
||||||
|
|
||||||
|
// Use today's weather if we dont have information for yesterday yet (i.e. on startup)
|
||||||
|
Object.assign( result, today, yesterday);
|
||||||
|
|
||||||
|
if ( "precip" in yesterday && "precip" in today ) {
|
||||||
|
result.precip = yesterday.precip + today.precip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PWS report "buckets" so consider it still raining if last bucket was less than an hour ago
|
||||||
|
if ( last_bucket !== undefined ) {
|
||||||
|
result.raining = ( ( Date.now() - +last_bucket ) / 1000 / 60 / 60 < 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
new CronJob( "0 0 0 * * *", function() {
|
||||||
|
|
||||||
|
yesterday = Object.assign( {}, today );
|
||||||
|
today = Object.assign( {} );
|
||||||
|
count.temp = 0; count.humidity = 0;
|
||||||
|
current_date = new Date();
|
||||||
|
}, null, true );
|
||||||
|
|
||||||
|
|
||||||
|
interface PWSStatus {
|
||||||
|
temp?: number;
|
||||||
|
humidity?: number;
|
||||||
|
precip?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocalWeather extends PWSStatus {
|
||||||
|
raining?: boolean;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as express from "express";
|
import * as express from "express";
|
||||||
|
import { AdjustmentOptions, GeoCoordinates, TimeData, WateringData, WeatherData } from "../types";
|
||||||
|
|
||||||
const http = require( "http" ),
|
const http = require( "http" ),
|
||||||
|
local = require( "./local"),
|
||||||
SunCalc = require( "suncalc" ),
|
SunCalc = require( "suncalc" ),
|
||||||
moment = require( "moment-timezone" ),
|
moment = require( "moment-timezone" ),
|
||||||
geoTZ = require( "geo-tz" ),
|
geoTZ = require( "geo-tz" ),
|
||||||
@@ -15,21 +17,26 @@ const http = require( "http" ),
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses the Weather Underground API to resolve a location name (ZIP code, city name, country name, etc.) to geographic
|
* Resolves a location description to geographic coordinates.
|
||||||
* coordinates.
|
* @param location A partial zip/city/country or a coordinate pair.
|
||||||
* @param location A zip code or partial city/country name.
|
|
||||||
* @return A promise that will be resolved with the coordinates of the best match for the specified location, or
|
* @return A promise that will be resolved with the coordinates of the best match for the specified location, or
|
||||||
* rejected with an error message if unable to resolve the location.
|
* rejected with an error message if unable to resolve the location.
|
||||||
*/
|
*/
|
||||||
async function resolveCoordinates( location: string ): Promise< GeoCoordinates > {
|
async function resolveCoordinates( location: string ): Promise< GeoCoordinates > {
|
||||||
|
|
||||||
|
if ( filters.pws.test( location ) ) {
|
||||||
|
throw "Unable to resolve location";
|
||||||
|
} else if ( filters.gps.test( location ) ) {
|
||||||
|
const split: string[] = location.split( "," );
|
||||||
|
return [ parseFloat( split[ 0 ] ), parseFloat( split[ 1 ] ) ];
|
||||||
|
} else {
|
||||||
// Generate URL for autocomplete request
|
// Generate URL for autocomplete request
|
||||||
const url = "http://autocomplete.wunderground.com/aq?h=0&query=" +
|
const url = "http://autocomplete.wunderground.com/aq?h=0&query=" +
|
||||||
encodeURIComponent( location );
|
encodeURIComponent( location );
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = await getData( url );
|
data = await httpJSONRequest( url );
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If the request fails, indicate no data was found.
|
// If the request fails, indicate no data was found.
|
||||||
throw "An API error occurred while attempting to resolve location";
|
throw "An API error occurred while attempting to resolve location";
|
||||||
@@ -46,14 +53,15 @@ async function resolveCoordinates( location: string ): Promise< GeoCoordinates >
|
|||||||
throw "Unable to resolve location";
|
throw "Unable to resolve location";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes an HTTP GET request to the specified URL and parses the JSON response body.
|
* Makes an HTTP/HTTPS GET request to the specified URL and parses the JSON response body.
|
||||||
* @param url The URL to fetch.
|
* @param url The URL to fetch.
|
||||||
* @return A Promise that will be resolved the with parsed response body if the request succeeds, or will be rejected
|
* @return A Promise that will be resolved the with parsed response body if the request succeeds, or will be rejected
|
||||||
* with an Error if the request or JSON parsing fails.
|
* with an Error if the request or JSON parsing fails.
|
||||||
*/
|
*/
|
||||||
async function getData( url: string ): Promise< any > {
|
async function httpJSONRequest(url: string ): Promise< any > {
|
||||||
try {
|
try {
|
||||||
const data: string = await httpRequest(url);
|
const data: string = await httpRequest(url);
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
@@ -66,17 +74,17 @@ async function getData( url: string ): Promise< any > {
|
|||||||
/**
|
/**
|
||||||
* Retrieves weather data necessary for watering level calculations from the OWM API.
|
* Retrieves weather data necessary for watering level calculations from the OWM API.
|
||||||
* @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 OWMWateringData if the API calls succeed, or undefined if an
|
* @return A Promise that will be resolved with WateringData if the API calls succeed, or resolved with undefined
|
||||||
* error occurs while retrieving the weather data.
|
* if an error occurs while retrieving the weather data.
|
||||||
*/
|
*/
|
||||||
async function getOWMWateringData( coordinates: GeoCoordinates ): Promise< OWMWateringData > {
|
async function getOWMWateringData( coordinates: GeoCoordinates ): Promise< WateringData > {
|
||||||
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 ];
|
||||||
|
|
||||||
// Perform the HTTP request to retrieve the weather data
|
// Perform the HTTP request to retrieve the weather data
|
||||||
let forecast;
|
let forecast;
|
||||||
try {
|
try {
|
||||||
forecast = await getData( forecastUrl );
|
forecast = await httpJSONRequest( forecastUrl );
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Indicate watering data could not be retrieved if an API error occurs.
|
// Indicate watering data could not be retrieved if an API error occurs.
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -109,18 +117,18 @@ async function getOWMWateringData( coordinates: GeoCoordinates ): Promise< OWMWa
|
|||||||
/**
|
/**
|
||||||
* Retrieves the current weather data from OWM for usage in the mobile app.
|
* Retrieves the current weather data from OWM for usage in the mobile app.
|
||||||
* @param coordinates The coordinates to retrieve the weather for
|
* @param coordinates The coordinates to retrieve the weather for
|
||||||
* @return A Promise that will be resolved with the OWMWeatherData if the API calls succeed, or undefined if
|
* @return A Promise that will be resolved with the WeatherData if the API calls succeed, or resolved with undefined
|
||||||
* an error occurs while retrieving the weather data.
|
* if an error occurs while retrieving the weather data.
|
||||||
*/
|
*/
|
||||||
async function getOWMWeatherData( coordinates: GeoCoordinates ): Promise< OWMWeatherData > {
|
async function getOWMWeatherData( coordinates: GeoCoordinates ): Promise< WeatherData > {
|
||||||
const OWM_API_KEY = process.env.OWM_API_KEY,
|
const OWM_API_KEY = process.env.OWM_API_KEY,
|
||||||
currentUrl = "http://api.openweathermap.org/data/2.5/weather?appid=" + OWM_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=" + OWM_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 {
|
||||||
current = await getData( currentUrl );
|
current = await httpJSONRequest( currentUrl );
|
||||||
forecast = await getData( forecastDailyUrl );
|
forecast = await httpJSONRequest( forecastDailyUrl );
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Indicate watering data could not be retrieved if an API error occurs.
|
// Indicate watering data could not be retrieved if an API error occurs.
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -131,7 +139,7 @@ async function getOWMWeatherData( coordinates: GeoCoordinates ): Promise< OWMWea
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const weather: OWMWeatherData = {
|
const weather: WeatherData = {
|
||||||
temp: parseInt( current.main.temp ),
|
temp: parseInt( current.main.temp ),
|
||||||
humidity: parseInt( current.main.humidity ),
|
humidity: parseInt( current.main.humidity ),
|
||||||
wind: parseInt( current.wind.speed ),
|
wind: parseInt( current.wind.speed ),
|
||||||
@@ -159,6 +167,15 @@ async function getOWMWeatherData( coordinates: GeoCoordinates ): Promise< OWMWea
|
|||||||
return weather;
|
return weather;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves weather data necessary for watering level calculations from the a local record.
|
||||||
|
* @param coordinates The coordinates to retrieve the watering data for.
|
||||||
|
* @return A Promise that will be resolved with WateringData.
|
||||||
|
*/
|
||||||
|
async function getLocalWateringData( coordinates: GeoCoordinates ): Promise< WateringData > {
|
||||||
|
return local.getLocalWeather();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates timezone and sunrise/sunset for the specified coordinates.
|
* Calculates timezone and sunrise/sunset for the specified coordinates.
|
||||||
* @param coordinates The coordinates to use to calculate time data.
|
* @param coordinates The coordinates to use to calculate time data.
|
||||||
@@ -189,7 +206,7 @@ function getTimeData( coordinates: GeoCoordinates ): TimeData {
|
|||||||
* @param wateringData The weather to use to calculate watering percentage.
|
* @param wateringData The weather to use to calculate watering percentage.
|
||||||
* @return The percentage that watering should be scaled by, or -1 if an invalid adjustmentMethod was provided.
|
* @return The percentage that watering should be scaled by, or -1 if an invalid adjustmentMethod was provided.
|
||||||
*/
|
*/
|
||||||
function calculateWeatherScale( adjustmentMethod: number, adjustmentOptions: AdjustmentOptions, wateringData: OWMWateringData ): number {
|
function calculateWeatherScale( adjustmentMethod: number, adjustmentOptions: AdjustmentOptions, wateringData: WateringData ): number {
|
||||||
|
|
||||||
// Zimmerman method
|
// Zimmerman method
|
||||||
if ( adjustmentMethod === 1 ) {
|
if ( adjustmentMethod === 1 ) {
|
||||||
@@ -244,7 +261,7 @@ function calculateWeatherScale( adjustmentMethod: number, adjustmentOptions: Adj
|
|||||||
* @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: OWMWateringData ): boolean {
|
function checkWeatherRestriction( adjustmentValue: number, weather: WateringData ): boolean {
|
||||||
|
|
||||||
const californiaRestriction = ( adjustmentValue >> 7 ) & 1;
|
const californiaRestriction = ( adjustmentValue >> 7 ) & 1;
|
||||||
|
|
||||||
@@ -263,28 +280,23 @@ function checkWeatherRestriction( adjustmentValue: number, weather: OWMWateringD
|
|||||||
|
|
||||||
exports.getWeatherData = async function( req: express.Request, res: express.Response ) {
|
exports.getWeatherData = async function( req: express.Request, res: express.Response ) {
|
||||||
const location: string = getParameter(req.query.loc);
|
const location: string = getParameter(req.query.loc);
|
||||||
|
|
||||||
|
if ( !location ) {
|
||||||
|
res.send( "Error: Unable to resolve location" );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let coordinates: GeoCoordinates;
|
let coordinates: GeoCoordinates;
|
||||||
|
|
||||||
if ( filters.gps.test( location ) ) {
|
|
||||||
|
|
||||||
// Handle GPS coordinates by storing each coordinate in an array
|
|
||||||
const split: string[] = location.split( "," );
|
|
||||||
coordinates = [ parseFloat( split[ 0 ] ), parseFloat( split[ 1 ] ) ];
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Attempt to resolve provided location to GPS coordinates when it does not match
|
|
||||||
// a GPS coordinate or Weather Underground location using Weather Underground autocomplete
|
|
||||||
try {
|
try {
|
||||||
coordinates = await resolveCoordinates( location );
|
coordinates = await resolveCoordinates( location );
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.send( "Error: Unable to resolve location" );
|
res.send( "Error: Unable to resolve location" );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Continue with the weather request
|
// Continue with the weather request
|
||||||
const timeData: TimeData = getTimeData( coordinates );
|
const timeData: TimeData = getTimeData( coordinates );
|
||||||
const weatherData: OWMWeatherData = await getOWMWeatherData( coordinates );
|
const weatherData: WeatherData = await getOWMWeatherData( coordinates );
|
||||||
|
|
||||||
res.json( {
|
res.json( {
|
||||||
...timeData,
|
...timeData,
|
||||||
@@ -363,7 +375,12 @@ exports.getWateringData = async function( req: express.Request, res: express.Res
|
|||||||
|
|
||||||
// Continue with the weather request
|
// Continue with the weather request
|
||||||
let timeData: TimeData = getTimeData( coordinates );
|
let timeData: TimeData = getTimeData( coordinates );
|
||||||
const wateringData: OWMWateringData = await getOWMWateringData( coordinates );
|
let wateringData: WateringData;
|
||||||
|
if ( local.useLocalWeather() ) {
|
||||||
|
wateringData = await getLocalWateringData( coordinates );
|
||||||
|
} else {
|
||||||
|
wateringData = await getOWMWateringData(coordinates);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Process data to retrieve the resulting scale, sunrise/sunset, timezone,
|
// Process data to retrieve the resulting scale, sunrise/sunset, timezone,
|
||||||
@@ -420,6 +437,10 @@ exports.getWateringData = async function( req: express.Request, res: express.Res
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ( local.useLocalWeather() ) {
|
||||||
|
console.log( "OpenSprinkler Weather Response: %s", JSON.stringify( data ) );
|
||||||
|
}
|
||||||
|
|
||||||
// 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" ) {
|
||||||
res.json( data );
|
res.json( data );
|
||||||
@@ -557,70 +578,3 @@ 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 || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Geographic coordinates. The 1st element is the latitude, and the 2nd element is the longitude. */
|
|
||||||
type GeoCoordinates = [number, number];
|
|
||||||
|
|
||||||
interface TimeData {
|
|
||||||
/** 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).
|
|
||||||
*/
|
|
||||||
timezone: number;
|
|
||||||
/** The time of sunrise, in minutes from UTC midnight. */
|
|
||||||
sunrise: number;
|
|
||||||
/** The time of sunset, in minutes from UTC midnight. */
|
|
||||||
sunset: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OWMWeatherData {
|
|
||||||
/** The current temperature (in Fahrenheit). */
|
|
||||||
temp: number;
|
|
||||||
/** The current humidity (as a percentage). */
|
|
||||||
humidity: number;
|
|
||||||
wind: number;
|
|
||||||
description: string;
|
|
||||||
icon: string;
|
|
||||||
region: string;
|
|
||||||
city: string;
|
|
||||||
minTemp: number;
|
|
||||||
maxTemp: number;
|
|
||||||
precip: number;
|
|
||||||
forecast: OWMWeatherDataForecast[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OWMWeatherDataForecast {
|
|
||||||
temp_min: number;
|
|
||||||
temp_max: number;
|
|
||||||
date: number;
|
|
||||||
icon: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OWMWateringData {
|
|
||||||
/** The average forecasted temperature over the next 30 hours (in Fahrenheit). */
|
|
||||||
temp: number;
|
|
||||||
/** The average forecasted humidity over the next 30 hours (as a percentage). */
|
|
||||||
humidity: number;
|
|
||||||
/** The forecasted total precipitation over the next 30 hours (in inches). */
|
|
||||||
precip: number;
|
|
||||||
/** A boolean indicating if it is currently raining. */
|
|
||||||
raining: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AdjustmentOptions {
|
|
||||||
/** Base humidity (as a percentage). */
|
|
||||||
bh?: number;
|
|
||||||
/** Base temperature (in Fahrenheit). */
|
|
||||||
bt?: number;
|
|
||||||
/** Base precipitation (in inches). */
|
|
||||||
br?: number;
|
|
||||||
/** The percentage to weight the humidity factor by. */
|
|
||||||
h?: number;
|
|
||||||
/** The percentage to weight the temperature factor by. */
|
|
||||||
t?: number;
|
|
||||||
/** The percentage to weight the precipitation factor by. */
|
|
||||||
r?: number;
|
|
||||||
/** The rain delay to use (in hours). */
|
|
||||||
d?: number;
|
|
||||||
}
|
|
||||||
|
|||||||
27
server.ts
27
server.ts
@@ -1,14 +1,19 @@
|
|||||||
var express = require( "express" ),
|
const packageJson = require( "../package.json" ),
|
||||||
|
express = require( "express" ),
|
||||||
weather = require( "./routes/weather.js" ),
|
weather = require( "./routes/weather.js" ),
|
||||||
cors = require( "cors" ),
|
local = require( "./routes/local.js" ),
|
||||||
host = process.env.HOST || "127.0.0.1",
|
cors = require( "cors" );
|
||||||
|
|
||||||
|
let host = process.env.HOST || "127.0.0.1",
|
||||||
port = process.env.PORT || 3000,
|
port = process.env.PORT || 3000,
|
||||||
|
pws = process.env.PWS || "none",
|
||||||
app = express();
|
app = express();
|
||||||
|
|
||||||
if ( !process.env.HOST || !process.env.PORT ) {
|
if ( !process.env.HOST || !process.env.PORT || !process.env.LOCAL_PWS ) {
|
||||||
require( "dotenv" ).load();
|
require( "dotenv" ).load();
|
||||||
host = process.env.HOST || host;
|
host = process.env.HOST || host;
|
||||||
port = process.env.PORT || port;
|
port = process.env.PORT || port;
|
||||||
|
pws = process.env.PWS || pws;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle requests matching /weatherID.py where ID corresponds to the
|
// Handle requests matching /weatherID.py where ID corresponds to the
|
||||||
@@ -21,8 +26,13 @@ app.get( /(\d+)/, weather.getWateringData );
|
|||||||
app.options( /weatherData/, cors() );
|
app.options( /weatherData/, cors() );
|
||||||
app.get( /weatherData/, cors(), weather.getWeatherData );
|
app.get( /weatherData/, cors(), weather.getWeatherData );
|
||||||
|
|
||||||
|
// Endpoint to stream Weather Underground data from local PWS
|
||||||
|
if ( pws === "WU" ) {
|
||||||
|
app.get( "/weatherstation/updateweatherstation.php", local.captureWUStream );
|
||||||
|
}
|
||||||
|
|
||||||
app.get( "/", function( req, res ) {
|
app.get( "/", function( req, res ) {
|
||||||
res.send( "OpenSprinkler Weather Service" );
|
res.send( packageJson.description + " v" + packageJson.version );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// Handle 404 error
|
// Handle 404 error
|
||||||
@@ -33,7 +43,12 @@ app.use( function( req, res ) {
|
|||||||
|
|
||||||
// Start listening on the service port
|
// Start listening on the service port
|
||||||
app.listen( port, host, function() {
|
app.listen( port, host, function() {
|
||||||
console.log( "OpenSprinkler Weather Service now listening on %s:%s", host, port );
|
console.log( "%s now listening on %s:%s", packageJson.description, host, port );
|
||||||
|
|
||||||
|
if (pws !== "none" ) {
|
||||||
|
console.log( "%s now listening for local weather stream", packageJson.description );
|
||||||
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
exports.app = app;
|
exports.app = app;
|
||||||
|
exports.pws = pws;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"server.ts",
|
"server.ts",
|
||||||
|
"types.ts",
|
||||||
"routes/**/*"
|
"routes/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
65
types.ts
Normal file
65
types.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/** Geographic coordinates. The 1st element is the latitude, and the 2nd element is the longitude. */
|
||||||
|
export type GeoCoordinates = [number, number];
|
||||||
|
|
||||||
|
export interface TimeData {
|
||||||
|
/** 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).
|
||||||
|
*/
|
||||||
|
timezone: number;
|
||||||
|
/** The time of sunrise, in minutes from UTC midnight. */
|
||||||
|
sunrise: number;
|
||||||
|
/** The time of sunset, in minutes from UTC midnight. */
|
||||||
|
sunset: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeatherData {
|
||||||
|
/** The current temperature (in Fahrenheit). */
|
||||||
|
temp: number;
|
||||||
|
/** The current humidity (as a percentage). */
|
||||||
|
humidity: number;
|
||||||
|
wind: number;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
region: string;
|
||||||
|
city: string;
|
||||||
|
minTemp: number;
|
||||||
|
maxTemp: number;
|
||||||
|
precip: number;
|
||||||
|
forecast: WeatherDataForecast[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeatherDataForecast {
|
||||||
|
temp_min: number;
|
||||||
|
temp_max: number;
|
||||||
|
date: number;
|
||||||
|
icon: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WateringData {
|
||||||
|
/** The average forecasted temperature over the next 30 hours (in Fahrenheit). */
|
||||||
|
temp: number;
|
||||||
|
/** The average forecasted humidity over the next 30 hours (as a percentage). */
|
||||||
|
humidity: number;
|
||||||
|
/** The forecasted total precipitation over the next 30 hours (in inches). */
|
||||||
|
precip: number;
|
||||||
|
/** A boolean indicating if it is currently raining. */
|
||||||
|
raining: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdjustmentOptions {
|
||||||
|
/** Base humidity (as a percentage). */
|
||||||
|
bh?: number;
|
||||||
|
/** Base temperature (in Fahrenheit). */
|
||||||
|
bt?: number;
|
||||||
|
/** Base precipitation (in inches). */
|
||||||
|
br?: number;
|
||||||
|
/** The percentage to weight the humidity factor by. */
|
||||||
|
h?: number;
|
||||||
|
/** The percentage to weight the temperature factor by. */
|
||||||
|
t?: number;
|
||||||
|
/** The percentage to weight the precipitation factor by. */
|
||||||
|
r?: number;
|
||||||
|
/** The rain delay to use (in hours). */
|
||||||
|
d?: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user