Then I got a Davis Vantage Pro 2 weather station for Father's day last June. I noticed the console displayed evapotranspiration data, but WeatherSnoop (which is otherwise awesome) didn't support it. I started doing a little digging in to how hard it would be to calculate it myself, and found a gold mine. Turns out the American Society of Civil Engineers decided to standardize evapotranspiration calculations about 10 years ago, and while the final report costs $30 to download, the draft (and all the appendices!) are downloadable for free from the University of Idaho.
Given how variable our local weather is, I opted to calculate the reference evapotranspiration constant based on hourly averages of weather data. I've had the thing working for a few days now, and it seems to agree with console calcs and common sense. If anyone who has a weather station that measures temperature, relative humidity, wind speed, and solar radiation wants to give it a try here it is, with apologies for my sad lack of Mad Python Skillz.
- Code: Select all
"""
Calculate hourly evapotranspiration data from a weather station equipped to measure:
Relative Humidity, Temperature, Wind Speed (e.g., Davis Vantage Pro 2)
Relies on a separate script to totalize relevant data from device "WeatherSnoop" at
1 minute intervals.
This script runs on the hour, calculates ETo (Evapotranspiration reference value)
for Use by irrigation scripts
The rational for conversion of watts per square meter to
and from megajoules per square meter per hour are covered here:
http://rredc.nrel.gov/solar/pubs/sbf/b.html
In brief: An average of 1 watt for a 1 hour period = 3.6 kJ/h
These calculations rely on the approach outlined by
THE ASCE STANDARDIZED REFERENCE EVAPOTRANSPIRATION EQUATION
developed by the American Society of Civil Engineers, specifically those
for hourly data, and references to equations (e.g., eq. 55) refer to the
equation numbers used in that document.
Draft versions of the ASCE report and the appendices are downloadable at no charge from:
http://www.kimberly.uidaho.edu/water/asceewri/
"""
import math
from math import exp
import time
# Global constants
PI = 3.14159265
alt = 11 #in meters
lat = 29.568 #in decimal degrees
long = 95.05 #in decimal degrees
LZ = 90 # set longitude for center of time zone
Zw = 2 # height of wind speed measurement in meters above ground level
GSC = 4.92 # Global Solar Constant in MJ m^-2 h^-1
sigma = 2.042e-10 # Stefan-Boltzmann constant (in MJ K^-4 m^-2 h^-1)
albedo = 0.23 # generally accepted value for canopy reflection coefficient
Cn = 37 # numerator constant that for grass/hourly time steps (Table 1)
Cd_day = 0.24
Cd_night = 0.96
# Load totals for the last hourly measurement period from Indigo variables
totalSolRad = float(indigo.variables[1514345125].value) # "totalSolRad"
totalTempC = float(indigo.variables[900917121].value) # "totalTempC"
totalHumid = float(indigo.variables[137383963].value) # "totalHumid"
totalPress = float(indigo.variables[1583976].value) # "totalPress"
totalWind = float(indigo.variables[27494067].value) # "totalWind"
totalSampleCount = int(indigo.variables[582955114].value) # "totalSampleCount"
dailyETo = float(indigo.variables[1849325147].value) # "dailyETo"
runningETo = float(indigo.variables[1374037415].value) # "runningETo"
# Read historical cloud factor in case sun is too low in sky to accurately
# calculate a new one from solar radiation data
Fcd = float(indigo.variables[76116845].value) # "cloudFactor"
# Calculate average values for input variables
avgSolRad = totalSolRad/totalSampleCount
avgTempC = totalTempC/totalSampleCount
avgHumid = totalHumid/totalSampleCount
avgPress = totalPress/totalSampleCount
avgWind = totalWind/totalSampleCount
indigo.server.log("avgSolRad = " + str(avgSolRad) + "W/m2")
indigo.server.log("avgTempC = " + str(avgTempC))
indigo.server.log("avgHumid = " + str(avgHumid))
indigo.server.log("avgPress = " + str(avgPress))
indigo.server.log("avgWind = " + str(avgWind))
# Calculate misc. conversions and date/time related info we will need
avgWind = 0.447 * avgWind # convert wind speed from MPH to m/s
avgTempK = avgTempC + 273.15 # Convert temperature to Kelvins
lt = time.localtime(time.time())
doy = lt[7] #Day of Year from time tuple
Rs = avgSolRad * 3.6e-3 # convert average wattage to megajoules per square meter per hour
indigo.server.log("Rs = " + str(Rs) + "MJ/m2h-1")
avgTempK = avgTempC + 273.15
P = 101.3 - 0.0115 * alt +5.44 * 10e-7 * alt**2 #atmospheric pressure from eq. 34
indigo.server.log("pressure = " + str(P))
# Correct wind speed measurement height to 2 meters using eq. 67
U2 = avgWind * (4.87/math.log(67.8 * Zw -5.42))
gamma = 0.000665 * P #Psychrometric constant from eq. 35
es = 0.6108 * exp(avgTempC*17.27/(avgTempC+237.3)) # saturation vapor pressure eq. 37
indigo.server.log("Psychrometric constant (gamma) = " + str(gamma))
# Using relative humidity data to calculate ea since weather station measures this
# and it is a "preferred method" as per ASCE
ea = es * avgHumid/100 # actual vapor pressure from eq. 41
# Calculate slope of saturation vapor pressure curve (delta) using eq. 36
delta = 2503 * exp(17.27 * avgTempC / (avgTempC + 237.3))/((avgTempC + 237.3)**2)
indigo.server.log("slope of vapor pressure (delta) = " + str(delta))
"""
this part gets a little ugly, because we have to figure out where the sun is in the sky
during the observation period, and deal with cases where either sunrise or sunset occur
during the observation period. We also need to correct for seasonal variation in when "solar
noon" occurs, and for daylight savings time.
"""
sd = 0.409 * math.sin(((2 * PI / 365) * doy - 1.39)) # solar declination from eq. 24
lat_rad = lat * (PI / 180) # convert latitude from degrees to radians
sha = math.acos(-math.tan(lat_rad) * math.tan(sd)) # sunset hour angle from eq. 59
indigo.server.log("solar declination = " + str(sd))
indigo.server.log("sunset hour angle = " + str(sha))
# Calculate solar time angle at midpoint of time period
# correct for daylight savings time
t = lt[3] - 1 + lt[8] #Need to check if this works when NOT in Daylight Savings Time
t = t - 0.5 # use midpoint of measurement period
# Calculate seasonal corrections using eq. 57 & 58
b = 2 * PI * (doy - 81)/364
SC = 0.1645 * math.sin(2 * b) - 0.1255 * math.cos(b) - 0.025 * math.sin (b)
# Calculate sun angles for beginning and end of period
# Also deal with cases where sun rose or set during observation period
omega = PI/12 * ((t + 0.06667 * (LZ - long) + SC) - 12) #eq. 55
omega1 = omega - PI/24 #eq. 53
omega2 = omega + PI/24 #eq. 54
# Correct for sunset and sunrise using eq. 56
# recall that sunrise hour angle = -1 * sunset hour angle
if omega1 < -1 * sha:
omega1 = -1 * sha
omega1Dark = True
elif omega1 > sha:
omega1 = sha
omega1Dark = True
else:
omega1Dark = False
if omega2 < -1 * sha:
omega2 = -1 * sha
omega2Dark = True
elif omega2 > sha:
omega2 = sha
omega2Dark = True
else:
omega2Dark = False
if omega1 > omega2:
omega1 = omega2
if omega1Dark:
if omega2Dark:
daytime = False
else:
daytime = True
else:
daytime = True
indigo.server.log("midpoint time t = " + str(t) + " hours")
indigo.server.log("seasonal correction = " + str(SC) + " hours")
indigo.server.log("omega = " + str(omega))
indigo.server.log("omega1 = " + str(omega1))
indigo.server.log("omega2 = " + str(omega2))
indigo.server.log("corrected sunset hour angle = " + str(sha))
indigo.server.log("daytime = " + str(daytime))
# Calculate sun angle above horizon
beta = math.asin(math.sin(lat_rad) * math.sin(sd) + math.cos(lat_rad) * math.cos(sd) * math.cos(omega))
indigo.server.log("current sun angle = " + str(beta) + " radians")
beta_deg = beta * (180 / PI)
indigo.server.log("current angle = " + str(beta_deg) + " degrees")
# Calculate extraterrestrial radiation for period using eq. 48
# But only if some observations occurred during daylight hours
if daytime:
IRD = 1 + (0.033 * math.cos((2 * PI / 365)* doy)) # inverse relative distance (earth to sun)
Ra = (12/PI) * GSC * IRD * ((omega2 - omega1) * math.sin(lat_rad) * math.sin(sd) + math.cos(lat_rad) * math.cos(sd) * (math.sin(omega2) - math.sin (omega1)))
indigo.server.log("Ra = " + str(Ra))
#Calculate clear sky radiation using eq. 47
Rso = Ra * (0.75 + 2e-5 * alt)
indigo.server.log("Rso = " + str(Rso))
# Calculate cloudiness function using eq. 45
# But only if the sun is high enough in the sky to allow meaningful solar radiation measurement
if beta > 0.3:
Fcd = (1.35 * (Rs/Rso)) - 0.35
indigo.variable.updateValue (indigo.variables[76116845], str(Fcd)) # "cloudFactor"
indigo.server.log("Updated Fcd = " + str(Fcd))
if Fcd <= 0.05:
Fcd = 0.05
indigo.server.log("using lower limit value for Fcd = " + str(Fcd))
elif Fcd > 1: #This shouldn't happen, warn user if it does
Fcd = 1 #actual radiation can't be greater than clear sky radiation
indigo.server.log("WARNING - using upper limit value for Fcd = " + str(Fcd))
indigo.variable.updateValue (indigo.variables[76116845], str(Fcd)) # "cloudFactor"
else: # sun too low - use stored value for cloudiness factor
indigo.server.log("using stored value for Fcd = " + str(Fcd))
# Calculate net long-wave radiation radiated upward using eq. 44
Rnl = sigma * Fcd * (0.34 - 0.14 * math.sqrt(ea)) * (avgTempK ** 4)
indigo.server.log("Rnl = " + str(Rnl) + " MJ/m2h-1")
Rns = Rs * (1- albedo) # correct shortwave radiation for albedo of reference crop
indigo.server.log("Rns = " + str(Rns) + " MJ/m2h-1")
Rn = Rns - Rnl # Calculate net radiation
indigo.server.log("Rn = " + str(Rn) + " MJ/m2h-1")
# Calculate Soil Heat Flux Density (G) using eqs. 65a, 65b (grass reference crop)
# Use different values depending on whether there was any solar radiation in the period
if Rn > 0:
G = 0.1 * Rn
else:
G = 0.5 * Rn
#And FINALLY we can calculate the evapotranspiration for this hour (ET) using eq. 1
if Rn > 0: # Use daytime value for Cd
ET = (0.408 * delta * (Rn - G) + gamma * (Cn/(avgTempC + 273) * U2 * (es - ea))) / (delta + (gamma * (1 + Cd_day * U2)))
else: # Use nighttime value for Cd
ET = (0.408 * delta * (Rn - G) + gamma * (Cn/(avgTempC + 273) * U2 * (es - ea))) / (delta + (gamma * (1 + Cd_night * U2)))
ET_in = (ET / 25.4)
indigo.server.log("Current hourly ETo (in mm) = " + str(ET))
indigo.server.log("Current hourly ETo (in inches) = " + str(ET_in))
# Update indigo variables
dailyETo = dailyETo + ET_in
runningETo = runningETo + ET_in
indigo.server.log("Current daily ETo (in inches) = " + str(dailyETo))
indigo.server.log("Current running total ETo (in inches) = " + str(runningETo))
indigo.variable.updateValue (indigo.variables[196835831], str(ET_in)) # "hourlyETo"
indigo.variable.updateValue (indigo.variables[1849325147], str(dailyETo)) # "dailyETo"
indigo.variable.updateValue (indigo.variables[1374037415],str(runningETo)) # "runningETo"
# re-zero indigo running totals for observation period
indigo.variable.updateValue (indigo.variables[1514345125], str(0)) # "totalSolRad"
indigo.variable.updateValue (indigo.variables[900917121], str(0)) # "totalTempC"
indigo.variable.updateValue (indigo.variables[137383963], str(0)) # "totalHumid"
indigo.variable.updateValue (indigo.variables[1583976], str(0)) # "totalPress"
indigo.variable.updateValue (indigo.variables[27494067], str(0)) # "totalWind"
indigo.variable.updateValue (indigo.variables[582955114], str(0)) # "totalSampleCount"
You'll also need to totalize your hourly weather station data (or pull it from your weatherstation DB).
Comments, suggestions, bug reports welcomed.