Merge pull request #82 from PeteBa/add-solarradition-to-local
Updated Local Weather Provider
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ baselineEToData/*.bin
|
|||||||
baselineEToData/*.png
|
baselineEToData/*.png
|
||||||
baselineEToData/*.tif
|
baselineEToData/*.tif
|
||||||
baselineEToData/dataPreparer[.exe]
|
baselineEToData/dataPreparer[.exe]
|
||||||
|
observations.json
|
||||||
|
|||||||
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
@@ -1,6 +1,21 @@
|
|||||||
{
|
{
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch via NPM",
|
||||||
|
"cwd": "${workspaceRoot}",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"windows": {
|
||||||
|
"runtimeExecutable": "npm.cmd"
|
||||||
|
},
|
||||||
|
"runtimeArgs": [
|
||||||
|
"run-script", "debug"
|
||||||
|
],
|
||||||
|
"port": 9229,
|
||||||
|
"preLaunchTask": "NPM Compile"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
|
|||||||
6
.vscode/tasks.json
vendored
6
.vscode/tasks.json
vendored
@@ -13,6 +13,12 @@
|
|||||||
"label": "development",
|
"label": "development",
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "NPM Compile",
|
||||||
|
"type": "npm",
|
||||||
|
"script": "compile",
|
||||||
|
"problemMatcher": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"test": "mocha --exit --require ts-node/register **/*.spec.ts",
|
"test": "mocha --exit --require ts-node/register **/*.spec.ts",
|
||||||
"start": "node js/server",
|
"start": "node js/server",
|
||||||
"dev": "node scripts/serve",
|
"dev": "node scripts/serve",
|
||||||
|
"debug": "node --inspect=9229 js/server",
|
||||||
"bundle": "npm run compile && zip weather.zip -r js package.json package-lock.json",
|
"bundle": "npm run compile && zip weather.zip -r js package.json package-lock.json",
|
||||||
"deploy": "npm run bundle && eb deploy",
|
"deploy": "npm run bundle && eb deploy",
|
||||||
"compile": "tsc"
|
"compile": "tsc"
|
||||||
|
|||||||
@@ -1,81 +1,155 @@
|
|||||||
import * as express from "express";
|
import * as express from "express";
|
||||||
import { CronJob } from "cron";
|
import * as moment from "moment";
|
||||||
import { GeoCoordinates, ZimmermanWateringData } from "../../types";
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
import { GeoCoordinates, WeatherData, ZimmermanWateringData } from "../../types";
|
||||||
import { WeatherProvider } from "./WeatherProvider";
|
import { WeatherProvider } from "./WeatherProvider";
|
||||||
|
import { EToData } from "../adjustmentMethods/EToAdjustmentMethod";
|
||||||
|
import { CodedError, ErrorCode } from "../../errors";
|
||||||
|
|
||||||
const count = { temp: 0, humidity: 0 };
|
var queue: Array<Observation> = [],
|
||||||
|
lastRainEpoch = 0,
|
||||||
|
lastRainCount: number;
|
||||||
|
|
||||||
let today: PWSStatus = {},
|
function getMeasurement(req: express.Request, key: string): number {
|
||||||
yesterday: PWSStatus = {},
|
let value: number;
|
||||||
last_bucket: Date,
|
|
||||||
current_date: Date = new Date();
|
|
||||||
|
|
||||||
function sameDay(d1: Date, d2: Date): boolean {
|
return ( key in req.query ) && !isNaN( value = parseFloat( req.query[key] ) ) && ( value !== -9999.0 ) ? value : undefined;
|
||||||
return d1.getFullYear() === d2.getFullYear() &&
|
|
||||||
d1.getMonth() === d2.getMonth() &&
|
|
||||||
d1.getDate() === d2.getDate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const captureWUStream = function( req: express.Request, res: express.Response ) {
|
export const captureWUStream = async function( req: express.Request, res: express.Response ) {
|
||||||
let prev: number, curr: number;
|
let rainCount = getMeasurement(req, "dailyrainin");
|
||||||
|
|
||||||
if ( !( "dateutc" in req.query ) || !sameDay( current_date, new Date( req.query.dateutc + "Z") )) {
|
const obs: Observation = {
|
||||||
res.send( "Error: Bad date range\n" );
|
timestamp: req.query.dateutc === "now" ? moment().unix() : moment( req.query.dateutc + "Z" ).unix(),
|
||||||
return;
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
if ( ( "tempf" in req.query ) && !isNaN( curr = parseFloat( req.query.tempf ) ) && curr !== -9999.0 ) {
|
lastRainEpoch = getMeasurement(req, "rainin") > 0 ? obs.timestamp : lastRainEpoch;
|
||||||
prev = ( "temp" in today ) ? today.temp : 0;
|
lastRainCount = isNaN(rainCount) ? lastRainCount : rainCount;
|
||||||
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 ) );
|
queue.unshift(obs);
|
||||||
|
|
||||||
res.send( "success\n" );
|
res.send( "success\n" );
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class LocalWeatherProvider extends WeatherProvider {
|
export default class LocalWeatherProvider extends WeatherProvider {
|
||||||
|
|
||||||
public async getWateringData( coordinates: GeoCoordinates ): Promise< ZimmermanWateringData > {
|
public async getWeatherData( coordinates: GeoCoordinates ): Promise< WeatherData > {
|
||||||
const result: ZimmermanWateringData = {
|
queue = queue.filter( obs => moment().unix() - obs.timestamp < 24*60*60 );
|
||||||
// Use today's weather if we don't have information for yesterday yet (i.e. on startup)
|
|
||||||
...today,
|
if ( queue.length == 0 ) {
|
||||||
// Use yesterday's weather updated every midnight, if available after startup
|
console.error( "There is insufficient data to support Weather response from local PWS." );
|
||||||
...yesterday as ZimmermanWateringData,
|
throw "There is insufficient data to support Weather response from local PWS.";
|
||||||
// PWS report "buckets" so consider it still raining if last bucket was less than an hour ago
|
}
|
||||||
raining: last_bucket !== undefined ? ( ( Date.now() - +last_bucket ) / 1000 / 60 / 60 < 1 ) : undefined,
|
|
||||||
weatherProvider: "local"
|
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: []
|
||||||
};
|
};
|
||||||
|
|
||||||
if ( "precip" in yesterday && "precip" in today ) {
|
return weather;
|
||||||
result.precip = yesterday.precip + today.precip;
|
}
|
||||||
|
|
||||||
|
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;
|
return result;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
new CronJob( "0 0 0 * * *", function() {
|
function saveQueue() {
|
||||||
|
queue = queue.filter( obs => moment().unix() - obs.timestamp < 24*60*60 );
|
||||||
yesterday = Object.assign( {}, today );
|
try {
|
||||||
today = Object.assign( {} );
|
fs.writeFileSync( "observations.json" , JSON.stringify( queue ), "utf8" );
|
||||||
count.temp = 0; count.humidity = 0;
|
} catch ( err ) {
|
||||||
current_date = new Date();
|
console.error( "Error saving historical observations to local storage.", err );
|
||||||
}, null, true );
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface PWSStatus {
|
if ( process.env.WEATHER_PROVIDER === "local" && process.env.LOCAL_PERSISTENCE ) {
|
||||||
temp?: number;
|
if ( fs.existsSync( "observations.json" ) ) {
|
||||||
humidity?: number;
|
try {
|
||||||
precip?: number;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user