Base: Add support for weather adjustments options

This commit is contained in:
Samer Albahra
2015-06-05 21:45:37 -05:00
parent b4fa0f3350
commit 75e167522b

View File

@@ -1,8 +1,19 @@
#!/usr/bin/python #!/usr/bin/python
import urllib, urllib2, cgi, re, math import urllib
import json, datetime, time, sys, calendar import urllib2
import pytz, ephem import cgi
import re
import math
import json
import datetime
import time
import sys
import calendar
import pytz
import ephem
from datetime import datetime, timedelta, date from datetime import datetime, timedelta, date
def safe_float(s, dv): def safe_float(s, dv):
r = dv r = dv
try: try:
@@ -11,6 +22,7 @@ def safe_float(s, dv):
return dv return dv
return r return r
def safe_int(s, dv): def safe_int(s, dv):
r = dv r = dv
try: try:
@@ -19,6 +31,7 @@ def safe_int(s, dv):
return dv return dv
return r return r
def isInt(s): def isInt(s):
try: try:
_v = int(s) _v = int(s)
@@ -26,6 +39,7 @@ def isInt(s):
return 0 return 0
return 1 return 1
def isFloat(s): def isFloat(s):
try: try:
_f = float(s) _f = float(s)
@@ -33,35 +47,42 @@ def isFloat(s):
return 0 return 0
return 1 return 1
def F2C(temp): def F2C(temp):
return (temp-32)*5/9 return (temp - 32) * 5 / 9
def C2F(temp): def C2F(temp):
return temp*9/5 + 32 return temp * 9 / 5 + 32
def mm2in(x): def mm2in(x):
return x*0.03937008 return x * 0.03937008
def ft2m(x): def ft2m(x):
return x*0.3048 return x * 0.3048
def IP2Int(ip): def IP2Int(ip):
o = map(int, ip.split('.')) o = map(int, ip.split('.'))
res = (16777216 * o[0]) + (65536 * o[1]) + (256 * o[2]) + o[3] res = (16777216 * o[0]) + (65536 * o[1]) + (256 * o[2]) + o[3]
return res return res
def getClientAddress(environ): def getClientAddress(environ):
try: try:
return environ['HTTP_X_FORWARDED_FOR'].split(',')[-1].strip() return environ['HTTP_X_FORWARDED_FOR'].split(',')[-1].strip()
except KeyError: except KeyError:
return environ['REMOTE_ADDR'] return environ['REMOTE_ADDR']
def computeETs(latitude, longitude, elevation, temp_high, temp_low, temp_avg, hum_high, hum_low, hum_avg, wind, solar): def computeETs(latitude, longitude, elevation, temp_high, temp_low, temp_avg, hum_high, hum_low, hum_avg, wind, solar):
tm = time.gmtime() tm = time.gmtime()
dayofyear = tm.tm_yday dayofyear = tm.tm_yday
latitude = safe_float(latitude, 0); latitude = safe_float(latitude, 0)
longitude = safe_float(longitude, 0); longitude = safe_float(longitude, 0)
# Converted values # Converted values
El = ft2m(elevation) El = ft2m(elevation)
@@ -72,97 +93,116 @@ def computeETs(latitude, longitude, elevation, temp_high, temp_low, temp_avg, hu
RHx = float(hum_high) RHx = float(hum_high)
RHn = float(hum_low) RHn = float(hum_low)
RHm = float(hum_avg) RHm = float(hum_avg)
Td = Tm-(100-RHm)/5 # approx. dewpoint (daily mean) Td = Tm - (100 - RHm) / 5 # approx. dewpoint (daily mean)
U2 = float(wind) * 0.44704 # wind speed in m/s U2 = float(wind) * 0.44704 # wind speed in m/s
# Step 1: Extraterrestrial radiation # Step 1: Extraterrestrial radiation
Gsc = 0.082 Gsc = 0.082
sigma = 4.90e-9 sigma = 4.90e-9
phi = math.pi * latitude / 180 phi = math.pi * latitude / 180
dr = 1 + 0.033*math.cos(2*math.pi*dayofyear/365) dr = 1 + 0.033 * math.cos(2 * math.pi * dayofyear / 365)
delta = 0.409*math.sin(2*math.pi*dayofyear/365 - 1.39) delta = 0.409 * math.sin(2 * math.pi * dayofyear / 365 - 1.39)
omegas = math.acos(-math.tan(phi)*math.tan(delta)) omegas = math.acos(-math.tan(phi) * math.tan(delta))
Ra = (24*60/math.pi)*Gsc*dr*(omegas*math.sin(delta)*math.sin(phi)+math.cos(phi)*math.cos(delta)*math.sin(omegas)) Ra = (24 * 60 / math.pi) * Gsc * dr * (omegas * math.sin(delta)
* math.sin(phi) + math.cos(phi) * math.cos(delta) * math.sin(omegas))
# Step 2: Daily net radiation # Step 2: Daily net radiation
Rso = Ra*(0.75 + 2.0e-5 * El) # 5 Rso = Ra * (0.75 + 2.0e-5 * El) # 5
Rns = (1-0.23)*Rs Rns = (1 - 0.23) * Rs
f = 1.35*Rs/Rso - 0.35 # 7 f = 1.35 * Rs / Rso - 0.35 # 7
esTx = 0.6108*math.exp(17.27*Tx/(Tx+237.3)) # 8 esTx = 0.6108 * math.exp(17.27 * Tx / (Tx + 237.3)) # 8
esTn = 0.6108*math.exp(17.27*Tn/(Tn+237.3)) esTn = 0.6108 * math.exp(17.27 * Tn / (Tn + 237.3))
ed = (esTx*RHn/100 + esTn*RHx/100)/2 # 10 ed = (esTx * RHn / 100 + esTn * RHx / 100) / 2 # 10
ea = (esTx+esTn)/2 # 22 ea = (esTx + esTn) / 2 # 22
epsilonp = 0.34-0.14*math.sqrt(ea) # 12 epsilonp = 0.34 - 0.14 * math.sqrt(ea) # 12
Rnl = -f*epsilonp*sigma*((Tx+273.14)**4 + (Tn+273.15)**4)/2 # 13 Rnl = -f * epsilonp * sigma * \
((Tx + 273.14) ** 4 + (Tn + 273.15) ** 4) / 2 # 13
Rn = Rns + Rnl Rn = Rns + Rnl
# Step 3: variables needed to compute ET # Step 3: variables needed to compute ET
beta = 101.3*((293-0.0065*El)/293)**5.26 # 15 beta = 101.3 * ((293 - 0.0065 * El) / 293) ** 5.26 # 15
lam = 2.45 lam = 2.45
gamma = 0.00163*beta/lam gamma = 0.00163 * beta / lam
e0 = 0.6108*math.exp(17.27*Tm/(Tm+237.3)) # 19 e0 = 0.6108 * math.exp(17.27 * Tm / (Tm + 237.3)) # 19
Delta = 4099*e0/(Tm+237.3)**2 # 20 Delta = 4099 * e0 / (Tm + 237.3) ** 2 # 20
G = 0 G = 0
ea = (esTx+esTn)/2 ea = (esTx + esTn) / 2
# Step 4: calculate ETh # Step 4: calculate ETh
ETh = 0.408*(0.0023*Ra*(Tm+17.8)*math.sqrt(Tx-Tn)) # 23 ETh = 0.408 * (0.0023 * Ra * (Tm + 17.8) * math.sqrt(Tx - Tn)) # 23
# Step 5: calculate ET0 # Step 5: calculate ET0
R0 = 0.408*Delta*(Rn-G)/(Delta+gamma*(1+0.34*U2)) # 24 R0 = 0.408 * Delta * (Rn - G) / (Delta + gamma * (1 + 0.34 * U2)) # 24
A0 = (900*gamma/(Tm+273))*U2*(ea-ed) / (Delta+gamma*(1+0.34*U2)) # 25 A0 = (900 * gamma / (Tm + 273)) * U2 * (ea - ed) / \
(Delta + gamma * (1 + 0.34 * U2)) # 25
ET0 = R0 + A0 ET0 = R0 + A0
# Step 6: calculate ETr # Step 6: calculate ETr
Rr = 0.408*Delta*(Rn-G) / (Delta+gamma*(1+0.38*U2)) # 27 Rr = 0.408 * Delta * (Rn - G) / (Delta + gamma * (1 + 0.38 * U2)) # 27
Ar = (1600*gamma/(Tm+273))*U2*(ea-ed) / (Delta+gamma*(1+0.38*U2)) # 28 Ar = (1600 * gamma / (Tm + 273)) * U2 * (ea - ed) / \
(Delta + gamma * (1 + 0.38 * U2)) # 28
ETr = Rr + Ar ETr = Rr + Ar
return (mm2in(ETh), mm2in(ET0), mm2in(ETr)) return (mm2in(ETh), mm2in(ET0), mm2in(ETr))
def not_found(environ, start_response):
"""Called if no URL matches."""
start_response('404 NOT FOUND', [('Content-Type', 'text/plain')])
return ['Not Found']
def application(environ, start_response): def application(environ, start_response):
path = environ.get('PATH_INFO') path = environ.get('PATH_INFO')
uwt = re.match('/weather(\d+)\.py',path) uwt = re.match('/weather(\d+)\.py', path)
parameters = cgi.parse_qs(environ.get('QUERY_STRING', '')) parameters = cgi.parse_qs(environ.get('QUERY_STRING', ''))
status = '200 OK' status = '200 OK'
wto = {}
if uwt: if uwt is not None:
uwt = safe_int(uwt.group(1),0) uwt = safe_int(uwt.group(1), 0)
else: else:
uwt = 0 return not_found(environ, start_response)
if 'loc' in parameters: if 'loc' in parameters:
loc = parameters['loc'][0] loc = parameters['loc'][0]
else: else:
loc = '' loc = ''
if 'key' in parameters: if 'key' in parameters:
key = parameters['key'][0] key = parameters['key'][0]
else: else:
key = '' key = ''
if 'format' in parameters: if 'format' in parameters:
of = parameters['format'][0] of = parameters['format'][0]
else: else:
of = '' of = ''
if 'fwv' in parameters: if 'fwv' in parameters:
fwv = parameters['fwv'][0] fwv = parameters['fwv'][0]
else: else:
fwv = '' fwv = ''
solar, wind, avehumidity, minhumidity, maxhumidity, maxt, mint, elevation, restrict, maxh, minh, meant, pre, pre_today, h_today, sunrise, sunset, scale, toffset = [0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -500, -1, -1, -1, -1, -1, -1, -1] if 'wto' in parameters:
ET = [0,0,0] wto = json.loads('{' + parameters['wto'][0] + '}')
solar, wind, avehumidity, minhumidity, maxhumidity, maxt, mint, elevation, restrict, maxh, minh, meant, pre, pre_today, h_today, sunrise, sunset, scale, toffset = [
0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -500, -1, -1, -1, -1, -1, -1, -1]
ET = [0, 0, 0]
eip = IP2Int(getClientAddress(environ)) eip = IP2Int(getClientAddress(environ))
# if loc is GPS coordinate itself # if loc is GPS coordinate itself
sp = loc.split(',', 1) sp = loc.split(',', 1)
if len(sp)==2 and isFloat(sp[0]) and isFloat(sp[1]): if len(sp) == 2 and isFloat(sp[0]) and isFloat(sp[1]):
lat = sp[0] lat = sp[0]
lon = sp[1] lon = sp[1]
else: else:
@@ -171,23 +211,27 @@ def application(environ, start_response):
# if loc is US 5+4 zip code, strip the last 4 # if loc is US 5+4 zip code, strip the last 4
sp = loc.split('-', 1) sp = loc.split('-', 1)
if len(sp)==2 and isInt(sp[0]) and len(sp[0])==5 and isInt(sp[1]) and len(sp[1])==4: if len(sp) == 2 and isInt(sp[0]) and len(sp[0]) == 5 and isInt(sp[1]) and len(sp[1]) == 4:
loc=sp[0] loc = sp[0]
tzone = None tzone = None
# if loc is pws, query wunderground geolookup to get GPS coordinates # if loc is pws, query wunderground geolookup to get GPS coordinates
if loc.startswith('pws:') or loc.startswith('icao:'): if loc.startswith('pws:') or loc.startswith('icao:'):
try: try:
req = urllib2.urlopen('http://api.wunderground.com/api/'+key+'/conditions/forecast/q/'+urllib.quote(loc)+'.json') req = urllib2.urlopen('http://api.wunderground.com/api/' +
key + '/conditions/forecast/q/' + urllib.quote(loc) + '.json')
dat = json.load(req) dat = json.load(req)
if 'current_observation' in dat: if 'current_observation' in dat:
v = dat['current_observation']['observation_location']['latitude'] v = dat['current_observation'][
'observation_location']['latitude']
if v and isFloat(v): if v and isFloat(v):
lat = v lat = v
v = dat['current_observation']['observation_location']['longitude'] v = dat['current_observation'][
'observation_location']['longitude']
if v and isFloat(v): if v and isFloat(v):
lon = v lon = v
v = dat['current_observation']['observation_location']['elevation'] v = dat['current_observation'][
'observation_location']['elevation']
if v: if v:
elevation = safe_int(int(v.split()[0]), 0) elevation = safe_int(int(v.split()[0]), 0)
v = dat['current_observation']['solarradiation'] v = dat['current_observation']['solarradiation']
@@ -228,9 +272,10 @@ def application(environ, start_response):
tzone = None tzone = None
# now do autocomplete lookup to get GPS coordinates # now do autocomplete lookup to get GPS coordinates
if lat==None or lon==None: if lat == None or lon == None:
try: try:
req = urllib2.urlopen('http://autocomplete.wunderground.com/aq?h=0&query='+urllib.quote(loc)) req = urllib2.urlopen(
'http://autocomplete.wunderground.com/aq?h=0&query=' + urllib.quote(loc))
dat = json.load(req) dat = json.load(req)
if dat['RESULTS']: if dat['RESULTS']:
v = dat['RESULTS'][0]['lat'] v = dat['RESULTS'][0]['lat']
@@ -254,7 +299,7 @@ def application(environ, start_response):
if (lat) and (lon): if (lat) and (lon):
if not loc.startswith('pws:') and not loc.startswith('icao:'): if not loc.startswith('pws:') and not loc.startswith('icao:'):
loc = ''+lat+','+lon loc = '' + lat + ',' + lon
home = ephem.Observer() home = ephem.Observer()
@@ -264,20 +309,23 @@ def application(environ, start_response):
sun = ephem.Sun() sun = ephem.Sun()
sun.compute(home) sun.compute(home)
sunrise = calendar.timegm(home.next_rising(sun).datetime().utctimetuple()) sunrise = calendar.timegm(
sunset = calendar.timegm(home.next_setting(sun).datetime().utctimetuple()) home.next_rising(sun).datetime().utctimetuple())
sunset = calendar.timegm(
home.next_setting(sun).datetime().utctimetuple())
if tzone: if tzone:
try: try:
tnow = pytz.utc.localize(datetime.utcnow()) tnow = pytz.utc.localize(datetime.utcnow())
tdelta = tnow.astimezone(pytz.timezone(tzone)).utcoffset() tdelta = tnow.astimezone(pytz.timezone(tzone)).utcoffset()
toffset = tdelta.days*96+tdelta.seconds/900+48; toffset = tdelta.days * 96 + tdelta.seconds / 900 + 48
except: except:
toffset=-1 toffset = -1
if (key != ''): if (key != ''):
try: try:
req = urllib2.urlopen('http://api.wunderground.com/api/'+key+'/yesterday/conditions/q/'+urllib.quote(loc)+'.json') req = urllib2.urlopen('http://api.wunderground.com/api/' +
key + '/yesterday/conditions/q/' + urllib.quote(loc) + '.json')
dat = json.load(req) dat = json.load(req)
if dat['history'] and dat['history']['dailysummary']: if dat['history'] and dat['history']['dailysummary']:
@@ -300,46 +348,61 @@ def application(environ, start_response):
v = info['precip_today_in'] v = info['precip_today_in']
if v: if v:
pre_today = safe_float(v, pre_today) pre_today = safe_float(v, pre_today)
v = info['relative_humidity'].replace('%','') v = info['relative_humidity'].replace('%', '')
if v: if v:
h_today = safe_float(v, h_today) h_today = safe_float(v, h_today)
# Check which weather method is being used # Check which weather method is being used
if ((uwt & ~(1 << 7)) == 1): if ((uwt & ~(1 << 7)) == 1):
# calculate water time scale, per https://github.com/rszimm/sprinklers_pi/blob/master/Weather.cpp # calculate water time scale, per
# https://github.com/rszimm/sprinklers_pi/blob/master/Weather.cpp
hf = 0 hf = 0
if (maxh>=0) and (minh>=0): if (maxh >= 0) and (minh >= 0):
hf = 30 - (maxh+minh)/2 hf = 30 - (maxh + minh) / 2
#elif (h_today>=0): # elif (h_today>=0):
# hf = 30 - h_today # hf = 30 - h_today
tf = 0 tf = 0
if (meant > -500): if (meant > -500):
tf = (meant - 70) * 4 tf = (meant - 70) * 4
rf = 0 rf = 0
if (pre>=0): if (pre >= 0):
rf -= pre * 200 rf -= pre * 200
if (pre_today>=0): if (pre_today >= 0):
rf -= pre_today * 200 rf -= pre_today * 200
if 'temp' in wto:
tf = tf * (wto['temp'] / 100.0)
if 'humidity' in wto:
hf = hf * (wto['humidity'] / 100.0)
if 'rain' in wto:
rf = rf * (wto['rain'] / 100.0)
scale = (int)(100 + hf + tf + rf) scale = (int)(100 + hf + tf + rf)
if (scale<0): if (scale < 0):
scale = 0 scale = 0
if (scale>200): if (scale > 200):
scale = 200 scale = 200
elif ((uwt & ~(1 << 7)) == 2): elif ((uwt & ~(1 << 7)) == 2):
ET = computeETs(lat, lon, elevation, maxt, mint, meant, maxhumidity, minhumidity, avehumidity, wind, solar) ET = computeETs(lat, lon, elevation, maxt, mint, meant,
# TODO: Actually generate correct scale using ET (ET[1] is ET0 for short canopy) maxhumidity, minhumidity, avehumidity, wind, solar)
# TODO: Actually generate correct scale using ET (ET[1] is ET0
# for short canopy)
scale = safe_int(ET[1] * 100, -1) scale = safe_int(ET[1] * 100, -1)
# Check weather modifier bits and apply scale modification # Check weather modifier bits and apply scale modification
if ((uwt>>7) & 1): if ((uwt >> 7) & 1):
# California modification to prevent watering when rain has occured within 48 hours # California modification to prevent watering when rain has
# occured within 48 hours
# Get before yesterday's weather data # Get before yesterday's weather data
beforeYesterday = date.today() - timedelta(2) beforeYesterday = date.today() - timedelta(2)
req = urllib2.urlopen('http://api.wunderground.com/api/'+key+'/history_'+beforeYesterday.strftime('%Y%m%d')+'/q/'+urllib.quote(loc)+'.json') req = urllib2.urlopen('http://api.wunderground.com/api/' + key + '/history_' +
beforeYesterday.strftime('%Y%m%d') + '/q/' + urllib.quote(loc) + '.json')
dat = json.load(req) dat = json.load(req)
if dat['history'] and dat['history']['dailysummary']: if dat['history'] and dat['history']['dailysummary']:
@@ -356,24 +419,28 @@ def application(environ, start_response):
except: except:
pass pass
urllib2.urlopen('https://ssl.google-analytics.com/collect?v=1&t=event&ec=weather&ea=lookup&el=results&ev='+str(scale)+'&cd1='+str(fwv)+'&cd2='+urllib.quote(loc)+'&cd3='+str(toffset)+'&cid=555&tid=UA-57507808-1&z='+str(time.time())) urllib2.urlopen('https://ssl.google-analytics.com/collect?v=1&t=event&ec=weather&ea=lookup&el=results&ev=' + str(scale) +
'&cd1=' + str(fwv) + '&cd2=' + urllib.quote(loc) + '&cd3=' + str(toffset) + '&cid=555&tid=UA-57507808-1&z=' + str(time.time()))
else: else:
urllib2.urlopen('https://ssl.google-analytics.com/collect?v=1&t=event&ec=timezone&ea=lookup&el=results&ev='+str(toffset)+'&cd1='+str(fwv)+'&cd2='+urllib.quote(loc)+'&cid=555&tid=UA-57507808-1&z='+str(time.time())) urllib2.urlopen('https://ssl.google-analytics.com/collect?v=1&t=event&ec=timezone&ea=lookup&el=results&ev=' + str(
toffset) + '&cd1=' + str(fwv) + '&cd2=' + urllib.quote(loc) + '&cid=555&tid=UA-57507808-1&z=' + str(time.time()))
# prepare sunrise sunset time # prepare sunrise sunset time
delta = 3600/4*(toffset-48) delta = 3600 / 4 * (toffset - 48)
if (sunrise >= 0): if (sunrise >= 0):
sunrise = int(((sunrise+delta)%86400)/60) sunrise = int(((sunrise + delta) % 86400) / 60)
if (sunset >= 0): if (sunset >= 0):
sunset = int(((sunset +delta)%86400)/60) sunset = int(((sunset + delta) % 86400) / 60)
if of=='json' or of=='JSON': if of == 'json' or of == 'JSON':
output = '{"scale":%d, "restrict":%d, "tz":%d, "sunrise":%d, "sunset":%d, "maxh":%d, "minh":%d, "meant":%d, "pre":%f, "prec":%f, "hc":%d, "eip":%d}' % (scale, restrict, toffset, sunrise, sunset, int(maxh), int(minh), int(meant), pre, pre_today, int(h_today), eip) output = '{"scale":%d, "restrict":%d, "tz":%d, "sunrise":%d, "sunset":%d, "maxh":%d, "minh":%d, "meant":%d, "pre":%f, "prec":%f, "hc":%d, "eip":%d}' % (
scale, restrict, toffset, sunrise, sunset, int(maxh), int(minh), int(meant), pre, pre_today, int(h_today), eip)
else: else:
output = '&scale=%d&restrict=%d&tz=%d&sunrise=%d&sunset=%d&maxh=%d&minh=%d&meant=%d&pre=%f&prec=%f&hc=%d&eip=%d' % (scale, restrict, toffset, sunrise, sunset, int(maxh), int(minh), int(meant), pre, pre_today, int(h_today), eip) output = '&scale=%d&restrict=%d&tz=%d&sunrise=%d&sunset=%d&maxh=%d&minh=%d&meant=%d&pre=%f&prec=%f&hc=%d&eip=%d' % (
scale, restrict, toffset, sunrise, sunset, int(maxh), int(minh), int(meant), pre, pre_today, int(h_today), eip)
response_headers = [('Content-type', 'text/plain'),('Content-Length', str(len(output)))] response_headers = [
('Content-type', 'text/plain'), ('Content-Length', str(len(output)))]
start_response(status, response_headers) start_response(status, response_headers)
return [output] return [output]