Files

156 lines
6.0 KiB
TypeScript

import * as express from "express";
import * as moment from "moment";
import * as fs from "fs";
import { GeoCoordinates, WeatherData, ZimmermanWateringData } from "../../types";
import { WeatherProvider } from "./WeatherProvider";
import { EToData } from "../adjustmentMethods/EToAdjustmentMethod";
import { CodedError, ErrorCode } from "../../errors";
var queue: Array<Observation> = [],
lastRainEpoch = 0,
lastRainCount: number;
function getMeasurement(req: express.Request, key: string): number {
let value: number;
return ( key in req.query ) && !isNaN( value = parseFloat( req.query[key] ) ) && ( value !== -9999.0 ) ? value : undefined;
}
export const captureWUStream = async function( req: express.Request, res: express.Response ) {
let rainCount = getMeasurement(req, "dailyrainin");
const obs: Observation = {
timestamp: req.query.dateutc === "now" ? moment().unix() : moment( req.query.dateutc + "Z" ).unix(),
temp: getMeasurement(req, "tempf"),
humidity: getMeasurement(req, "humidity"),
windSpeed: getMeasurement(req, "windspeedmph"),
solarRadiation: getMeasurement(req, "solarradiation") * 24 / 1000, // Convert to kWh/m^2 per day
precip: rainCount < lastRainCount ? rainCount : rainCount - lastRainCount,
};
lastRainEpoch = getMeasurement(req, "rainin") > 0 ? obs.timestamp : lastRainEpoch;
lastRainCount = isNaN(rainCount) ? lastRainCount : rainCount;
queue.unshift(obs);
res.send( "success\n" );
};
export default class LocalWeatherProvider extends WeatherProvider {
public async getWeatherData( coordinates: GeoCoordinates ): Promise< WeatherData > {
queue = queue.filter( obs => moment().unix() - obs.timestamp < 24*60*60 );
if ( queue.length == 0 ) {
console.error( "There is insufficient data to support Weather response from local PWS." );
throw "There is insufficient data to support Weather response from local PWS.";
}
const weather: WeatherData = {
weatherProvider: "local",
temp: Math.floor( queue[ 0 ].temp ) || undefined,
minTemp: undefined,
maxTemp: undefined,
humidity: Math.floor( queue[ 0 ].humidity ) || undefined ,
wind: Math.floor( queue[ 0 ].windSpeed * 10 ) / 10 || undefined,
precip: Math.floor( queue.reduce( ( sum, obs ) => sum + ( obs.precip || 0 ), 0) * 100 ) / 100,
description: "",
icon: "01d",
region: undefined,
city: undefined,
forecast: []
};
return weather;
}
public async getWateringData( coordinates: GeoCoordinates ): Promise< ZimmermanWateringData > {
queue = queue.filter( obs => moment().unix() - obs.timestamp < 24*60*60 );
if ( queue.length == 0 || queue[ 0 ].timestamp - queue[ queue.length - 1 ].timestamp < 23*60*60 ) {
console.error( "There is insufficient data to support Zimmerman calculation from local PWS." );
throw new CodedError( ErrorCode.InsufficientWeatherData );
}
let cTemp = 0, cHumidity = 0, cPrecip = 0;
const result: ZimmermanWateringData = {
weatherProvider: "local",
temp: queue.reduce( ( sum, obs ) => !isNaN( obs.temp ) && ++cTemp ? sum + obs.temp : sum, 0) / cTemp,
humidity: queue.reduce( ( sum, obs ) => !isNaN( obs.humidity ) && ++cHumidity ? sum + obs.humidity : sum, 0) / cHumidity,
precip: queue.reduce( ( sum, obs ) => !isNaN( obs.precip ) && ++cPrecip ? sum + obs.precip : sum, 0),
raining: ( ( moment().unix() - lastRainEpoch ) / 60 / 60 < 1 ),
};
if ( !( cTemp && cHumidity && cPrecip ) ) {
console.error( "There is insufficient data to support Zimmerman calculation from local PWS." );
throw new CodedError( ErrorCode.InsufficientWeatherData );
}
return result;
};
public async getEToData( coordinates: GeoCoordinates ): Promise< EToData > {
queue = queue.filter( obs => moment().unix() - obs.timestamp < 24*60*60 );
if ( queue.length == 0 || queue[ 0 ].timestamp - queue[ queue.length - 1 ].timestamp < 23*60*60 ) {
console.error( "There is insufficient data to support ETo calculation from local PWS." );
throw new CodedError( ErrorCode.InsufficientWeatherData );
}
let cSolar = 0, cWind = 0, cPrecip = 0;
const result: EToData = {
weatherProvider: "local",
periodStartTime: Math.floor( queue[ queue.length - 1 ].timestamp ),
minTemp: queue.reduce( (min, obs) => ( min > obs.temp ) ? obs.temp : min, Infinity ),
maxTemp: queue.reduce( (max, obs) => ( max < obs.temp ) ? obs.temp : max, -Infinity ),
minHumidity: queue.reduce( (min, obs) => ( min > obs.humidity ) ? obs.humidity : min, Infinity ),
maxHumidity: queue.reduce( (max, obs) => ( max < obs.humidity ) ? obs.humidity : max, -Infinity ),
solarRadiation: queue.reduce( (sum, obs) => !isNaN( obs.solarRadiation ) && ++cSolar ? sum + obs.solarRadiation : sum, 0) / cSolar,
windSpeed: queue.reduce( (sum, obs) => !isNaN( obs.windSpeed ) && ++cWind ? sum + obs.windSpeed : sum, 0) / cWind,
precip: queue.reduce( (sum, obs) => !isNaN( obs.precip ) && ++cPrecip ? sum + obs.precip : sum, 0 ),
};
if ( [ result.minTemp, result.minHumidity, -result.maxTemp, -result.maxHumidity ].includes( Infinity ) ||
!( cSolar && cWind && cPrecip ) ) {
console.error( "There is insufficient data to support ETo calculation from local PWS." );
throw new CodedError( ErrorCode.InsufficientWeatherData );
}
return result;
};
}
function saveQueue() {
queue = queue.filter( obs => moment().unix() - obs.timestamp < 24*60*60 );
try {
fs.writeFileSync( "observations.json" , JSON.stringify( queue ), "utf8" );
} catch ( err ) {
console.error( "Error saving historical observations to local storage.", err );
}
}
if ( process.env.WEATHER_PROVIDER === "local" && process.env.LOCAL_PERSISTENCE ) {
if ( fs.existsSync( "observations.json" ) ) {
try {
queue = JSON.parse( fs.readFileSync( "observations.json", "utf8" ) );
queue = queue.filter( obs => moment().unix() - obs.timestamp < 24*60*60 );
} catch ( err ) {
console.error( "Error reading historical observations from local storage.", err );
queue = [];
}
}
setInterval( saveQueue, 1000 * 60 * 30 );
}
interface Observation {
timestamp: number;
temp: number;
humidity: number;
windSpeed: number;
solarRadiation: number;
precip: number;
}