Initial version of Node.js rewrite
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
WeatherService.zip
|
||||||
45
Gruntfile.js
Normal file
45
Gruntfile.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
module.exports = function( grunt ) {
|
||||||
|
|
||||||
|
// Load node-modules;
|
||||||
|
grunt.loadNpmTasks( "grunt-contrib-jshint" );
|
||||||
|
grunt.loadNpmTasks( "grunt-contrib-compress" );
|
||||||
|
grunt.loadNpmTasks( "grunt-jscs" );
|
||||||
|
|
||||||
|
// Project configuration.
|
||||||
|
grunt.initConfig( {
|
||||||
|
pkg: grunt.file.readJSON( "package.json" ),
|
||||||
|
|
||||||
|
jshint: {
|
||||||
|
main: [ "server.js", "routes/**" ],
|
||||||
|
options: {
|
||||||
|
jshintrc: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
jscs: {
|
||||||
|
main: [ "server.js", "routes/**" ],
|
||||||
|
options: {
|
||||||
|
config: true,
|
||||||
|
fix: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
compress: {
|
||||||
|
build: {
|
||||||
|
options: {
|
||||||
|
archive: "WeatherService.zip"
|
||||||
|
},
|
||||||
|
files: [ {
|
||||||
|
src: [ ".ebextensions/*", "routes/*", "server.js", "package.json" ],
|
||||||
|
expand: true
|
||||||
|
} ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Default task(s).
|
||||||
|
grunt.registerTask( "default", [ "jshint", "jscs" ] );
|
||||||
|
grunt.registerTask( "build", [ "jshint", "jscs", "compress:build" ] );
|
||||||
|
|
||||||
|
};
|
||||||
446
application.py
446
application.py
@@ -1,446 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
import urllib
|
|
||||||
import urllib2
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def safe_float(s, dv):
|
|
||||||
r = dv
|
|
||||||
try:
|
|
||||||
r = float(s)
|
|
||||||
except:
|
|
||||||
return dv
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
def safe_int(s, dv):
|
|
||||||
r = dv
|
|
||||||
try:
|
|
||||||
r = int(s)
|
|
||||||
except:
|
|
||||||
return dv
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
def isInt(s):
|
|
||||||
try:
|
|
||||||
_v = int(s)
|
|
||||||
except:
|
|
||||||
return 0
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
def isFloat(s):
|
|
||||||
try:
|
|
||||||
_f = float(s)
|
|
||||||
except:
|
|
||||||
return 0
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
def F2C(temp):
|
|
||||||
return (temp - 32) * 5 / 9
|
|
||||||
|
|
||||||
|
|
||||||
def C2F(temp):
|
|
||||||
return temp * 9 / 5 + 32
|
|
||||||
|
|
||||||
|
|
||||||
def mm2in(x):
|
|
||||||
return x * 0.03937008
|
|
||||||
|
|
||||||
|
|
||||||
def ft2m(x):
|
|
||||||
return x * 0.3048
|
|
||||||
|
|
||||||
|
|
||||||
def IP2Int(ip):
|
|
||||||
o = map(int, ip.split('.'))
|
|
||||||
res = (16777216 * o[0]) + (65536 * o[1]) + (256 * o[2]) + o[3]
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def getClientAddress(environ):
|
|
||||||
try:
|
|
||||||
return environ['HTTP_X_FORWARDED_FOR'].split(',')[-1].strip()
|
|
||||||
except KeyError:
|
|
||||||
return environ['REMOTE_ADDR']
|
|
||||||
|
|
||||||
|
|
||||||
def computeETs(latitude, longitude, elevation, temp_high, temp_low, temp_avg, hum_high, hum_low, hum_avg, wind, solar):
|
|
||||||
tm = time.gmtime()
|
|
||||||
dayofyear = tm.tm_yday
|
|
||||||
|
|
||||||
latitude = safe_float(latitude, 0)
|
|
||||||
longitude = safe_float(longitude, 0)
|
|
||||||
|
|
||||||
# Converted values
|
|
||||||
El = ft2m(elevation)
|
|
||||||
Rs = float(solar) * 0.0864 # W/m2 to MJ/d /m2
|
|
||||||
Tx = F2C(float(temp_high))
|
|
||||||
Tn = F2C(float(temp_low))
|
|
||||||
Tm = F2C(float(temp_avg))
|
|
||||||
RHx = float(hum_high)
|
|
||||||
RHn = float(hum_low)
|
|
||||||
RHm = float(hum_avg)
|
|
||||||
Td = Tm - (100 - RHm) / 5 # approx. dewpoint (daily mean)
|
|
||||||
U2 = float(wind) * 0.44704 # wind speed in m/s
|
|
||||||
|
|
||||||
# Step 1: Extraterrestrial radiation
|
|
||||||
|
|
||||||
Gsc = 0.082
|
|
||||||
sigma = 4.90e-9
|
|
||||||
phi = math.pi * latitude / 180
|
|
||||||
dr = 1 + 0.033 * math.cos(2 * math.pi * dayofyear / 365)
|
|
||||||
delta = 0.409 * math.sin(2 * math.pi * dayofyear / 365 - 1.39)
|
|
||||||
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))
|
|
||||||
|
|
||||||
# Step 2: Daily net radiation
|
|
||||||
|
|
||||||
Rso = Ra * (0.75 + 2.0e-5 * El) # 5
|
|
||||||
Rns = (1 - 0.23) * Rs
|
|
||||||
f = 1.35 * Rs / Rso - 0.35 # 7
|
|
||||||
|
|
||||||
esTx = 0.6108 * math.exp(17.27 * Tx / (Tx + 237.3)) # 8
|
|
||||||
esTn = 0.6108 * math.exp(17.27 * Tn / (Tn + 237.3))
|
|
||||||
ed = (esTx * RHn / 100 + esTn * RHx / 100) / 2 # 10
|
|
||||||
ea = (esTx + esTn) / 2 # 22
|
|
||||||
|
|
||||||
epsilonp = 0.34 - 0.14 * math.sqrt(ea) # 12
|
|
||||||
Rnl = -f * epsilonp * sigma * \
|
|
||||||
((Tx + 273.14) ** 4 + (Tn + 273.15) ** 4) / 2 # 13
|
|
||||||
Rn = Rns + Rnl
|
|
||||||
|
|
||||||
# Step 3: variables needed to compute ET
|
|
||||||
|
|
||||||
beta = 101.3 * ((293 - 0.0065 * El) / 293) ** 5.26 # 15
|
|
||||||
lam = 2.45
|
|
||||||
gamma = 0.00163 * beta / lam
|
|
||||||
e0 = 0.6108 * math.exp(17.27 * Tm / (Tm + 237.3)) # 19
|
|
||||||
Delta = 4099 * e0 / (Tm + 237.3) ** 2 # 20
|
|
||||||
G = 0
|
|
||||||
ea = (esTx + esTn) / 2
|
|
||||||
|
|
||||||
# Step 4: calculate ETh
|
|
||||||
|
|
||||||
ETh = 0.408 * (0.0023 * Ra * (Tm + 17.8) * math.sqrt(Tx - Tn)) # 23
|
|
||||||
|
|
||||||
# Step 5: calculate ET0
|
|
||||||
|
|
||||||
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
|
|
||||||
ET0 = R0 + A0
|
|
||||||
|
|
||||||
# Step 6: calculate ETr
|
|
||||||
|
|
||||||
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
|
|
||||||
ETr = Rr + Ar
|
|
||||||
|
|
||||||
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):
|
|
||||||
path = environ.get('PATH_INFO')
|
|
||||||
uwt = re.match('/weather(\d+)\.py', path)
|
|
||||||
parameters = cgi.parse_qs(environ.get('QUERY_STRING', ''))
|
|
||||||
status = '200 OK'
|
|
||||||
wto = {}
|
|
||||||
|
|
||||||
if uwt is not None:
|
|
||||||
uwt = safe_int(uwt.group(1), 0)
|
|
||||||
else:
|
|
||||||
return not_found(environ, start_response)
|
|
||||||
|
|
||||||
if 'loc' in parameters:
|
|
||||||
loc = parameters['loc'][0]
|
|
||||||
else:
|
|
||||||
loc = ''
|
|
||||||
|
|
||||||
if 'key' in parameters:
|
|
||||||
key = parameters['key'][0]
|
|
||||||
else:
|
|
||||||
key = ''
|
|
||||||
|
|
||||||
if 'format' in parameters:
|
|
||||||
of = parameters['format'][0]
|
|
||||||
else:
|
|
||||||
of = ''
|
|
||||||
|
|
||||||
if 'fwv' in parameters:
|
|
||||||
fwv = parameters['fwv'][0]
|
|
||||||
else:
|
|
||||||
fwv = ''
|
|
||||||
|
|
||||||
if 'wto' in parameters:
|
|
||||||
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))
|
|
||||||
|
|
||||||
# if loc is GPS coordinate itself
|
|
||||||
sp = loc.split(',', 1)
|
|
||||||
if len(sp) == 2 and isFloat(sp[0]) and isFloat(sp[1]):
|
|
||||||
lat = sp[0]
|
|
||||||
lon = sp[1]
|
|
||||||
else:
|
|
||||||
lat = None
|
|
||||||
lon = None
|
|
||||||
|
|
||||||
# if loc is US 5+4 zip code, strip the last 4
|
|
||||||
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:
|
|
||||||
loc = sp[0]
|
|
||||||
|
|
||||||
tzone = None
|
|
||||||
# if loc is pws, query wunderground geolookup to get GPS coordinates
|
|
||||||
if loc.startswith('pws:') or loc.startswith('icao:'):
|
|
||||||
try:
|
|
||||||
req = urllib2.urlopen('http://api.wunderground.com/api/' +
|
|
||||||
key + '/conditions/forecast/q/' + urllib.quote(loc) + '.json')
|
|
||||||
dat = json.load(req)
|
|
||||||
if 'current_observation' in dat:
|
|
||||||
v = dat['current_observation'][
|
|
||||||
'observation_location']['latitude']
|
|
||||||
if v and isFloat(v):
|
|
||||||
lat = v
|
|
||||||
v = dat['current_observation'][
|
|
||||||
'observation_location']['longitude']
|
|
||||||
if v and isFloat(v):
|
|
||||||
lon = v
|
|
||||||
v = dat['current_observation'][
|
|
||||||
'observation_location']['elevation']
|
|
||||||
if v:
|
|
||||||
elevation = safe_int(int(v.split()[0]), 0)
|
|
||||||
v = dat['current_observation']['solarradiation']
|
|
||||||
if v:
|
|
||||||
solar = safe_int(v, 0)
|
|
||||||
v = dat['current_observation']['local_tz_long']
|
|
||||||
if v:
|
|
||||||
tzone = v
|
|
||||||
else:
|
|
||||||
v = dat['current_observation']['local_tz_short']
|
|
||||||
if v:
|
|
||||||
tzone = v
|
|
||||||
|
|
||||||
forecast = dat['forecast']['simpleforecast']['forecastday'][0]
|
|
||||||
|
|
||||||
v = forecast['high']['fahrenheit']
|
|
||||||
if v:
|
|
||||||
maxt = safe_int(v, 0)
|
|
||||||
v = forecast['low']['fahrenheit']
|
|
||||||
if v:
|
|
||||||
mint = safe_int(v, 0)
|
|
||||||
v = forecast['avehumidity']
|
|
||||||
if v:
|
|
||||||
avehumidity = safe_int(v, 0)
|
|
||||||
v = forecast['maxhumidity']
|
|
||||||
if v:
|
|
||||||
maxhumidity = safe_int(v, 0)
|
|
||||||
v = forecast['minhumidity']
|
|
||||||
if v:
|
|
||||||
minhumidity = safe_int(v, 0)
|
|
||||||
v = forecast['avewind']['mph']
|
|
||||||
if v:
|
|
||||||
wind = safe_int(v, 0)
|
|
||||||
|
|
||||||
except:
|
|
||||||
lat = None
|
|
||||||
lon = None
|
|
||||||
tzone = None
|
|
||||||
|
|
||||||
# now do autocomplete lookup to get GPS coordinates
|
|
||||||
if lat == None or lon == None:
|
|
||||||
try:
|
|
||||||
req = urllib2.urlopen(
|
|
||||||
'http://autocomplete.wunderground.com/aq?h=0&query=' + urllib.quote(loc))
|
|
||||||
dat = json.load(req)
|
|
||||||
if dat['RESULTS']:
|
|
||||||
v = dat['RESULTS'][0]['lat']
|
|
||||||
if v and isFloat(v):
|
|
||||||
lat = v
|
|
||||||
v = dat['RESULTS'][0]['lon']
|
|
||||||
if v and isFloat(v):
|
|
||||||
lon = v
|
|
||||||
v = dat['RESULTS'][0]['tz']
|
|
||||||
if v:
|
|
||||||
tzone = v
|
|
||||||
else:
|
|
||||||
v = dat['RESULTS'][0]['tz_long']
|
|
||||||
if v:
|
|
||||||
tzone = v
|
|
||||||
|
|
||||||
except:
|
|
||||||
lat = None
|
|
||||||
lon = None
|
|
||||||
tzone = None
|
|
||||||
|
|
||||||
if (lat) and (lon):
|
|
||||||
if not loc.startswith('pws:') and not loc.startswith('icao:'):
|
|
||||||
loc = '' + lat + ',' + lon
|
|
||||||
|
|
||||||
home = ephem.Observer()
|
|
||||||
|
|
||||||
home.lat = lat
|
|
||||||
home.long = lon
|
|
||||||
|
|
||||||
sun = ephem.Sun()
|
|
||||||
sun.compute(home)
|
|
||||||
|
|
||||||
sunrise = calendar.timegm(
|
|
||||||
home.next_rising(sun).datetime().utctimetuple())
|
|
||||||
sunset = calendar.timegm(
|
|
||||||
home.next_setting(sun).datetime().utctimetuple())
|
|
||||||
|
|
||||||
if tzone:
|
|
||||||
try:
|
|
||||||
tnow = pytz.utc.localize(datetime.utcnow())
|
|
||||||
tdelta = tnow.astimezone(pytz.timezone(tzone)).utcoffset()
|
|
||||||
toffset = tdelta.days * 96 + tdelta.seconds / 900 + 48
|
|
||||||
except:
|
|
||||||
toffset = -1
|
|
||||||
|
|
||||||
if (key != ''):
|
|
||||||
try:
|
|
||||||
req = urllib2.urlopen('http://api.wunderground.com/api/' +
|
|
||||||
key + '/yesterday/conditions/q/' + urllib.quote(loc) + '.json')
|
|
||||||
dat = json.load(req)
|
|
||||||
|
|
||||||
if dat['history'] and dat['history']['dailysummary']:
|
|
||||||
info = dat['history']['dailysummary'][0]
|
|
||||||
if info:
|
|
||||||
v = info['maxhumidity']
|
|
||||||
if v:
|
|
||||||
maxh = safe_float(v, maxh)
|
|
||||||
v = info['minhumidity']
|
|
||||||
if v:
|
|
||||||
minh = safe_float(v, minh)
|
|
||||||
v = info['meantempi']
|
|
||||||
if v:
|
|
||||||
meant = safe_float(v, meant)
|
|
||||||
v = info['precipi']
|
|
||||||
if v:
|
|
||||||
pre = safe_float(v, pre)
|
|
||||||
info = dat['current_observation']
|
|
||||||
if info:
|
|
||||||
v = info['precip_today_in']
|
|
||||||
if v:
|
|
||||||
pre_today = safe_float(v, pre_today)
|
|
||||||
v = info['relative_humidity'].replace('%', '')
|
|
||||||
if v:
|
|
||||||
h_today = safe_float(v, h_today)
|
|
||||||
|
|
||||||
# Check which weather method is being used
|
|
||||||
if ((uwt & ~(1 << 7)) == 1):
|
|
||||||
# calculate water time scale, per
|
|
||||||
# https://github.com/rszimm/sprinklers_pi/blob/master/Weather.cpp
|
|
||||||
hf = 0
|
|
||||||
if (maxh >= 0) and (minh >= 0):
|
|
||||||
hf = 30 - (maxh + minh) / 2
|
|
||||||
# elif (h_today>=0):
|
|
||||||
# hf = 30 - h_today
|
|
||||||
tf = 0
|
|
||||||
if (meant > -500):
|
|
||||||
tf = (meant - 70) * 4
|
|
||||||
rf = 0
|
|
||||||
if (pre >= 0):
|
|
||||||
rf -= pre * 200
|
|
||||||
if (pre_today >= 0):
|
|
||||||
rf -= pre_today * 200
|
|
||||||
|
|
||||||
if 't' in wto:
|
|
||||||
tf = tf * (wto['t'] / 100.0)
|
|
||||||
|
|
||||||
if 'h' in wto:
|
|
||||||
hf = hf * (wto['h'] / 100.0)
|
|
||||||
|
|
||||||
if 'r' in wto:
|
|
||||||
rf = rf * (wto['r'] / 100.0)
|
|
||||||
|
|
||||||
scale = (int)(100 + hf + tf + rf)
|
|
||||||
|
|
||||||
if (scale < 0):
|
|
||||||
scale = 0
|
|
||||||
if (scale > 200):
|
|
||||||
scale = 200
|
|
||||||
|
|
||||||
elif ((uwt & ~(1 << 7)) == 2):
|
|
||||||
ET = computeETs(lat, lon, elevation, maxt, mint, meant,
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Check weather modifier bits and apply scale modification
|
|
||||||
if ((uwt >> 7) & 1):
|
|
||||||
# California modification to prevent watering when rain has
|
|
||||||
# occured within 48 hours
|
|
||||||
|
|
||||||
# Get before yesterday's weather data
|
|
||||||
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')
|
|
||||||
dat = json.load(req)
|
|
||||||
|
|
||||||
if dat['history'] and dat['history']['dailysummary']:
|
|
||||||
info = dat['history']['dailysummary'][0]
|
|
||||||
if info:
|
|
||||||
v = info['precipi']
|
|
||||||
if v:
|
|
||||||
pre_beforeYesterday = safe_float(v, -1)
|
|
||||||
|
|
||||||
preTotal = pre_today + pre + pre_beforeYesterday
|
|
||||||
|
|
||||||
if (preTotal > 0.01):
|
|
||||||
restrict = 1
|
|
||||||
except:
|
|
||||||
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()))
|
|
||||||
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()))
|
|
||||||
|
|
||||||
# prepare sunrise sunset time
|
|
||||||
delta = 3600 / 4 * (toffset - 48)
|
|
||||||
if (sunrise >= 0):
|
|
||||||
sunrise = int(((sunrise + delta) % 86400) / 60)
|
|
||||||
if (sunset >= 0):
|
|
||||||
sunset = int(((sunset + delta) % 86400) / 60)
|
|
||||||
|
|
||||||
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)
|
|
||||||
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)
|
|
||||||
|
|
||||||
response_headers = [
|
|
||||||
('Content-type', 'text/plain'), ('Content-Length', str(len(output)))]
|
|
||||||
start_response(status, response_headers)
|
|
||||||
|
|
||||||
return [output]
|
|
||||||
13
package.json
Normal file
13
package.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "os-weather-service",
|
||||||
|
"description": "OpenSprinkler Weather Service",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^1.2.0",
|
||||||
|
"express": "^4.13.0",
|
||||||
|
"grunt": "^0.4.5",
|
||||||
|
"grunt-contrib-compress": "^0.13.0",
|
||||||
|
"grunt-contrib-jshint": "^0.11.2",
|
||||||
|
"grunt-jscs": "^1.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pytz==2014.10
|
|
||||||
ephem==3.7.5.3
|
|
||||||
254
routes/weather.js
Normal file
254
routes/weather.js
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
// Define regex filters to match against location
|
||||||
|
var http = require( "http" ),
|
||||||
|
filters = {
|
||||||
|
gps: /^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/,
|
||||||
|
pws: /^(?:pws|icao):/,
|
||||||
|
url: /^https?:\/\/([\w\.-]+)(:\d+)?(\/.*)?$/,
|
||||||
|
time: /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})([+-])(\d{2})(\d{2})/
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generic HTTP request handler that parses the URL and uses the
|
||||||
|
// native Node.js http module to perform the request
|
||||||
|
function httpRequest( url, callback ) {
|
||||||
|
url = url.match( filters.url );
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
host: url[1],
|
||||||
|
port: url[2] || 80,
|
||||||
|
path: url[3]
|
||||||
|
};
|
||||||
|
|
||||||
|
http.get( options, function( response ) {
|
||||||
|
var data = "";
|
||||||
|
|
||||||
|
// Reassemble the data as it comes in
|
||||||
|
response.on( "data", function( chunk ) {
|
||||||
|
data += chunk;
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Once the data is completely received, return it to the callback
|
||||||
|
response.on( "end", function() {
|
||||||
|
callback( data );
|
||||||
|
} );
|
||||||
|
} ).on( "error", function() {
|
||||||
|
|
||||||
|
// If the HTTP request fails, return false
|
||||||
|
callback( false );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts IP string to integer
|
||||||
|
function ipToInt( ip ) {
|
||||||
|
ip = ip.split( "." );
|
||||||
|
return ( ( ( ( ( ( +ip[0] ) * 256 ) + ( +ip[1] ) ) * 256 ) + ( +ip[2] ) ) * 256 ) + ( +ip[3] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes a PWS or ICAO location and resolves the GPS coordinates
|
||||||
|
function getPWSCoordinates( location, weatherUndergroundKey, callback ) {
|
||||||
|
var url = "http://api.wunderground.com/api/" + weatherUndergroundKey +
|
||||||
|
"/conditions/forecast/q/" + encodeURIComponent( location ) + ".json";
|
||||||
|
|
||||||
|
httpRequest( url, function( data ) {
|
||||||
|
data = JSON.parse( data );
|
||||||
|
|
||||||
|
if ( typeof data === "object" && data.current_observation && data.current_observation.observation_location ) {
|
||||||
|
callback( [ data.current_observation.observation_location.latitude,
|
||||||
|
data.current_observation.observation_location.longitude ] );
|
||||||
|
} else {
|
||||||
|
callback( false );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
// If location does not match GPS or PWS/ICAO, then attempt to resolve
|
||||||
|
// location using Weather Underground autocomplete API
|
||||||
|
function resolveCoordinates( location, callback ) {
|
||||||
|
var url = "http://autocomplete.wunderground.com/aq?h=0&query=" +
|
||||||
|
encodeURIComponent( location );
|
||||||
|
|
||||||
|
httpRequest( url, function( data ) {
|
||||||
|
data = JSON.parse( data );
|
||||||
|
if ( data.hasOwnProperty( "RESULTS" ) ) {
|
||||||
|
callback( [ data.RESULTS[0].lat, data.RESULTS[0].lon ] );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accepts a time string formatted in ISO-8601 and returns the timezone.
|
||||||
|
// The timezone output is formatted for OpenSprinkler Unified firmware.
|
||||||
|
function getTimezone( time ) {
|
||||||
|
var time = time.match( filters.time ),
|
||||||
|
hour = parseInt( time[7] + time[8] ),
|
||||||
|
minute = parseInt( time[9] );
|
||||||
|
|
||||||
|
minute = ( minute / 15 >> 0 ) / 4.0;
|
||||||
|
hour = hour + ( hour >=0 ? minute : -minute );
|
||||||
|
|
||||||
|
return ( ( hour + 12 ) * 4 ) >> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Handler when using the weatherX.py where X represents the
|
||||||
|
// adjustment method which is encoded to also carry the watering
|
||||||
|
// restriction and therefore must be decoded
|
||||||
|
exports.getWeather = function( req, res ) {
|
||||||
|
var adjustmentMethod = req.params[0] & ~( 1 << 7 ),
|
||||||
|
adjustmentOptions = req.query.wto,
|
||||||
|
location = req.query.loc,
|
||||||
|
weatherUndergroundKey = req.query.key,
|
||||||
|
outputFormat = req.query.format,
|
||||||
|
firmwareVersion = req.query.fwv,
|
||||||
|
remoteAddress = req.headers[ "x-forwarded-for" ].split(",")[0] || req.connection.remoteAddress,
|
||||||
|
weather = {},
|
||||||
|
|
||||||
|
// After the location is resolved, this function will run to complete the weather request
|
||||||
|
getWeatherData = function() {
|
||||||
|
|
||||||
|
// Get the API key from the environment variables
|
||||||
|
var WSI_API_KEY = process.env.WSI_API_KEY,
|
||||||
|
|
||||||
|
// Generate URL using The Weather Company API v1 in Imperial units
|
||||||
|
url = "http://api.weather.com/v1/geocode/" + location[0] + "/" + location[1] +
|
||||||
|
"/observations/current.json?apiKey=" + WSI_API_KEY + "&language=en-US&units=e";
|
||||||
|
|
||||||
|
// Perform the HTTP request to retrieve the weather data
|
||||||
|
httpRequest( url, function( data ) {
|
||||||
|
weather = JSON.parse( data );
|
||||||
|
|
||||||
|
var scale = calculateWeatherScale(),
|
||||||
|
restrict = checkWeatherRestriction() ? 1 : 0,
|
||||||
|
sunData = getSunData(),
|
||||||
|
timezone = getTimezone( weather.observation.obs_time_local );
|
||||||
|
|
||||||
|
// Return the response to the client
|
||||||
|
if ( outputFormat === "json" ) {
|
||||||
|
res.json( {
|
||||||
|
scale: scale,
|
||||||
|
restrict: restrict,
|
||||||
|
tz: timezone,
|
||||||
|
sunrise: sunData[0],
|
||||||
|
sunset: sunData[1],
|
||||||
|
eip: ipToInt( remoteAddress )
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
res.send( "&scale=" + scale +
|
||||||
|
"&restrict=" + restrict +
|
||||||
|
"&tz=" + timezone +
|
||||||
|
"&sunrise=" + sunData[0] +
|
||||||
|
"&sunset=" + sunData[1] +
|
||||||
|
"&eip=" + ipToInt( remoteAddress )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
getSunData = function() {
|
||||||
|
|
||||||
|
// Sun times must be converted from strings into date objects and processed into minutes from midnight
|
||||||
|
// TODO: Need to use the timezone to adjust sun times
|
||||||
|
var sunrise = weather.observation.sunrise.match( filters.time ),
|
||||||
|
sunset = weather.observation.sunset.match( filters.time );
|
||||||
|
|
||||||
|
return [
|
||||||
|
parseInt( sunrise[4] ) * 60 + parseInt( sunrise[5] ),
|
||||||
|
parseInt( sunset[4] ) * 60 + parseInt( sunset[5] )
|
||||||
|
];
|
||||||
|
},
|
||||||
|
calculateWeatherScale = function() {
|
||||||
|
|
||||||
|
// Calculate the average temperature
|
||||||
|
var temp = ( weather.observation.imperial.temp_max_24hour + weather.observation.imperial.temp_min_24hour ) / 2,
|
||||||
|
|
||||||
|
// Relative humidity and if unavailable default to 0
|
||||||
|
rh = weather.observation.imperial.rh || 0,
|
||||||
|
|
||||||
|
// The absolute precipitation in the past 48 hours
|
||||||
|
precip = weather.observation.imperial.precip_2day;
|
||||||
|
|
||||||
|
if ( typeof temp !== "number" ) {
|
||||||
|
|
||||||
|
// If the maximum and minimum temperatures are not available then use the current temperature
|
||||||
|
temp = weather.observation.imperial.temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log( {
|
||||||
|
temp: temp,
|
||||||
|
humidity: rh,
|
||||||
|
precip_48hr: precip
|
||||||
|
} );
|
||||||
|
|
||||||
|
if ( adjustmentMethod == 1 ) {
|
||||||
|
|
||||||
|
// Zimmerman method
|
||||||
|
|
||||||
|
var humidityFactor = ( 30 - rh ),
|
||||||
|
tempFactor = ( ( temp - 70 ) * 4 ),
|
||||||
|
precipFactor = ( precip * -2 );
|
||||||
|
|
||||||
|
// Apply adjustment options if available
|
||||||
|
if ( adjustmentOptions ) {
|
||||||
|
if ( adjustmentOptions.hasOwnProperty( "h" ) ) {
|
||||||
|
humidityFactor = humidityFactor * ( adjustmentOptions.h / 100 );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( adjustmentOptions.hasOwnProperty( "t" ) ) {
|
||||||
|
tempFactor = tempFactor * ( adjustmentOptions.t / 100 );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( adjustmentOptions.hasOwnProperty( "r" ) ) {
|
||||||
|
precipFactor = precipFactor * ( adjustmentOptions.r / 100 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseInt( Math.min( Math.max( 0, 100 + humidityFactor + tempFactor + precipFactor ), 200 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
},
|
||||||
|
checkWeatherRestriction = function() {
|
||||||
|
var californiaRestriction = ( req.params[0] >> 7 ) & 1;
|
||||||
|
|
||||||
|
if ( californiaRestriction ) {
|
||||||
|
|
||||||
|
// If the California watering restriction is in use then prevent watering
|
||||||
|
// if more then 0.01" of rain has accumulated in the past 48 hours
|
||||||
|
if ( weather.observation.imperial.precip_2day > 0.01 ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse weather adjustment options
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Reconstruct JSON string from deformed controller output
|
||||||
|
adjustmentOptions = JSON.parse( "{" + adjustmentOptions + "}" );
|
||||||
|
} catch (err) {
|
||||||
|
adjustmentOptions = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse location string
|
||||||
|
if ( filters.gps.test( location ) ) {
|
||||||
|
|
||||||
|
// Handle GPS coordinates
|
||||||
|
location = location.split( "," );
|
||||||
|
location = [ parseFloat( location[0] ), parseFloat( location[1] ) ];
|
||||||
|
getWeatherData();
|
||||||
|
|
||||||
|
} else if ( filters.pws.test( location ) ) {
|
||||||
|
|
||||||
|
// Handle Weather Underground specific location
|
||||||
|
getPWSCoordinates( location, weatherUndergroundKey, function( result ) {
|
||||||
|
location = result;
|
||||||
|
getWeatherData();
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Attempt to resolve provided location to GPS coordinates
|
||||||
|
resolveCoordinates( location, function( result ) {
|
||||||
|
location = result;
|
||||||
|
getWeatherData();
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
21
server.js
Normal file
21
server.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
var express = require( "express" ),
|
||||||
|
weather = require( "./routes/weather.js" ),
|
||||||
|
port = process.env.PORT || 3000;
|
||||||
|
app = express();
|
||||||
|
|
||||||
|
// Handle requests matching /weatherID.py where ID corresponds to the
|
||||||
|
// weather adjustment method selector.
|
||||||
|
// This endpoint is considered deprecated and supported for prior firmware
|
||||||
|
app.get( /weather(\d+)\.py/, weather.getWeather );
|
||||||
|
|
||||||
|
// Handle 404 error
|
||||||
|
app.use( function( req, res ) {
|
||||||
|
res.status( 404 );
|
||||||
|
res.send( "Not found" );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Start listening on the service port
|
||||||
|
var server = app.listen( port, "127.0.0.1", function() {
|
||||||
|
|
||||||
|
console.log( "OpenSprinkler Weather Service now listening on port %s", port );
|
||||||
|
} );
|
||||||
Reference in New Issue
Block a user