Merge pull request #2 from OpenSprinkler/nodejs-rewrite
Application rewrite from Python to Node
This commit is contained in:
37
.ebextensions/deploy_npm.config
Normal file
37
.ebextensions/deploy_npm.config
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
files:
|
||||||
|
"/opt/elasticbeanstalk/env.vars" :
|
||||||
|
mode: "000775"
|
||||||
|
owner: root
|
||||||
|
group: users
|
||||||
|
content: |
|
||||||
|
export NPM_CONFIG_LOGLEVEL=error
|
||||||
|
export NODE_PATH=`ls -td /opt/elasticbeanstalk/node-install/node-* | head -1`/bin
|
||||||
|
"/opt/elasticbeanstalk/hooks/appdeploy/pre/50npm.sh" :
|
||||||
|
mode: "000775"
|
||||||
|
owner: root
|
||||||
|
group: users
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
. /opt/elasticbeanstalk/env.vars
|
||||||
|
function error_exit
|
||||||
|
{
|
||||||
|
eventHelper.py --msg "$1" --severity ERROR
|
||||||
|
exit $2
|
||||||
|
}
|
||||||
|
|
||||||
|
#install not-installed yet app node_modules
|
||||||
|
if [ ! -d "/var/node_modules" ]; then
|
||||||
|
mkdir /var/node_modules ;
|
||||||
|
fi
|
||||||
|
if [ -d /tmp/deployment/application ]; then
|
||||||
|
ln -s /var/node_modules /tmp/deployment/application/
|
||||||
|
fi
|
||||||
|
|
||||||
|
OUT=$([ -d "/tmp/deployment/application" ] && cd /tmp/deployment/application && $NODE_PATH/npm install 2>&1) || error_exit "Failed to run npm install. $OUT" $?
|
||||||
|
echo $OUT
|
||||||
|
"/opt/elasticbeanstalk/hooks/configdeploy/pre/50npm.sh" :
|
||||||
|
mode: "000666"
|
||||||
|
owner: root
|
||||||
|
group: users
|
||||||
|
content: |
|
||||||
|
#no need to run npm install during configdeploy
|
||||||
27
.ebextensions/mongodb.config
Normal file
27
.ebextensions/mongodb.config
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
files:
|
||||||
|
"/home/ec2-user/install_mongo.sh" :
|
||||||
|
mode: "0007555"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
echo "[MongoDB]
|
||||||
|
name=MongoDB Repository
|
||||||
|
baseurl=http://downloads-distro.mongodb.org/repo/redhat/os/x86_64
|
||||||
|
gpgcheck=0
|
||||||
|
enabled=1" | tee -a /etc/yum.repos.d/mongodb.repo
|
||||||
|
yum -y update
|
||||||
|
yum -y install mongodb-org-server mongodb-org-shell mongodb-org-tools
|
||||||
|
|
||||||
|
commands:
|
||||||
|
01install_mongo:
|
||||||
|
command: ./install_mongo.sh
|
||||||
|
cwd: /home/ec2-user
|
||||||
|
test: '[ ! -f /usr/bin/mongo ] && echo "MongoDB not installed"'
|
||||||
|
|
||||||
|
services:
|
||||||
|
sysvinit:
|
||||||
|
mongod:
|
||||||
|
enabled: true
|
||||||
|
ensureRunning: true
|
||||||
|
commands: ['01install_mongo']
|
||||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
WeatherService.zip
|
||||||
|
|
||||||
|
# Elastic Beanstalk Files
|
||||||
|
.elasticbeanstalk/*
|
||||||
|
!.elasticbeanstalk/*.cfg.yml
|
||||||
|
!.elasticbeanstalk/*.global.yml
|
||||||
21
.jshintrc
Normal file
21
.jshintrc
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"boss": true,
|
||||||
|
"curly": true,
|
||||||
|
"eqeqeq": true,
|
||||||
|
"eqnull": true,
|
||||||
|
"expr": true,
|
||||||
|
"immed": true,
|
||||||
|
"noarg": true,
|
||||||
|
"onevar": true,
|
||||||
|
"quotmark": "double",
|
||||||
|
"smarttabs": true,
|
||||||
|
"trailing": true,
|
||||||
|
"undef": true,
|
||||||
|
"unused": true,
|
||||||
|
"browser": true,
|
||||||
|
"node": true,
|
||||||
|
"sub": true,
|
||||||
|
"globals": {
|
||||||
|
"module": true
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Gruntfile.js
Normal file
21
Gruntfile.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module.exports = function( grunt ) {
|
||||||
|
|
||||||
|
// Load node-modules;
|
||||||
|
grunt.loadNpmTasks( "grunt-contrib-jshint" );
|
||||||
|
|
||||||
|
// Project configuration.
|
||||||
|
grunt.initConfig( {
|
||||||
|
pkg: grunt.file.readJSON( "package.json" ),
|
||||||
|
|
||||||
|
jshint: {
|
||||||
|
main: [ "Gruntfile.js", "server.js", "routes/*.js", "models/*.js" ],
|
||||||
|
options: {
|
||||||
|
jshintrc: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Default task(s).
|
||||||
|
grunt.registerTask( "default", [ "jshint" ] );
|
||||||
|
|
||||||
|
};
|
||||||
@@ -3,12 +3,14 @@
|
|||||||
## Description
|
## Description
|
||||||
This script is used by OpenSprinkler Unified Firmware to update the water level of the device. It also provides timezone information based on user location along with other local information (sunrise, sunset, daylights saving time, etc).
|
This script is used by OpenSprinkler Unified Firmware to update the water level of the device. It also provides timezone information based on user location along with other local information (sunrise, sunset, daylights saving time, etc).
|
||||||
|
|
||||||
The production version runs on Amazon Elastic Beanstalk (AWS EB) and therefore this package is tailored to be zipped and uploaded to AWS EB. The script is written in Python.
|
The production version runs on Amazon Elastic Beanstalk (AWS EB) and therefore this package is tailored to be zipped and uploaded to AWS EB. The script is written in Javascript for Node.JS.
|
||||||
|
|
||||||
## File Detail
|
## File Detail
|
||||||
**Requirements.txt** is used to define the required Python modules needed to run the script.
|
**server.js** is the primary file launching the API daemon.
|
||||||
|
|
||||||
**Application.py** parses the incoming URL and returns the appropriate values. The script defaults to URL format return however a 'format' parameter can be passed with the value 'json' in order to output JSON.
|
**routes/*.js** contains all the endpoints for the API service. Currently, only one exists for weather adjustment.
|
||||||
|
|
||||||
|
**models/*.js** contains all the database models used by the routes. Currently, only one exists to manage weather cache data.
|
||||||
|
|
||||||
## Privacy
|
## Privacy
|
||||||
|
|
||||||
|
|||||||
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]
|
|
||||||
14
models/Cache.js
Normal file
14
models/Cache.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
var mongoose = require( "mongoose" );
|
||||||
|
|
||||||
|
var cacheSchema = new mongoose.Schema( {
|
||||||
|
|
||||||
|
// Stores the current GPS location as unique for weather data cache
|
||||||
|
location: { type: String, unique: true },
|
||||||
|
|
||||||
|
// This is the end of day value for the humidity yesterday
|
||||||
|
yesterdayHumidity: Number,
|
||||||
|
currentHumidityTotal: Number,
|
||||||
|
currentHumidityCount: Number
|
||||||
|
} );
|
||||||
|
|
||||||
|
module.exports = mongoose.model( "Cache", cacheSchema );
|
||||||
16
package.json
Normal file
16
package.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "os-weather-service",
|
||||||
|
"description": "OpenSprinkler Weather Service",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"repository": "https://github.com/OpenSprinkler/Weather-Adjustments/",
|
||||||
|
"dependencies": {
|
||||||
|
"cron": "^1.0.9",
|
||||||
|
"dotenv": "^1.2.0",
|
||||||
|
"express": "^4.13.0",
|
||||||
|
"grunt": "^0.4.5",
|
||||||
|
"grunt-contrib-jshint": "^0.11.2",
|
||||||
|
"mongoose": "^4.0.6",
|
||||||
|
"suncalc": "^1.6.0",
|
||||||
|
"xml2js": "^0.4.9"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pytz==2014.10
|
|
||||||
ephem==3.7.5.3
|
|
||||||
492
routes/weather.js
Normal file
492
routes/weather.js
Normal file
@@ -0,0 +1,492 @@
|
|||||||
|
var http = require( "http" ),
|
||||||
|
SunCalc = require( "suncalc" ),
|
||||||
|
parseXML = require( "xml2js" ).parseString,
|
||||||
|
Cache = require( "../models/Cache" ),
|
||||||
|
|
||||||
|
// Define regex filters to match against location
|
||||||
|
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})/,
|
||||||
|
timezone: /^()()()()()()([+-])(\d{2})(\d{2})/
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 ) {
|
||||||
|
|
||||||
|
// Generate URL for autocomplete request
|
||||||
|
var url = "http://autocomplete.wunderground.com/aq?h=0&query=" +
|
||||||
|
encodeURIComponent( location );
|
||||||
|
|
||||||
|
httpRequest( url, function( data ) {
|
||||||
|
|
||||||
|
// Parse the reply for JSON data
|
||||||
|
data = JSON.parse( data );
|
||||||
|
|
||||||
|
// Check if the data is valid
|
||||||
|
if ( typeof data.RESULTS === "object" && data.RESULTS.length ) {
|
||||||
|
|
||||||
|
// If it is, reply with an array containing the GPS coordinates
|
||||||
|
callback( [ data.RESULTS[0].lat, data.RESULTS[0].lon ] );
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Otherwise, indicate no data was found
|
||||||
|
callback( false );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve weather data to complete the weather request using Weather Underground
|
||||||
|
function getWeatherUndergroundData( location, weatherUndergroundKey, callback ) {
|
||||||
|
|
||||||
|
// Generate URL using The Weather Company API v1 in Imperial units
|
||||||
|
var url = "http://api.wunderground.com/api/" + weatherUndergroundKey +
|
||||||
|
"/yesterday/conditions/q/" + encodeURIComponent( location ) + ".json";
|
||||||
|
|
||||||
|
// Perform the HTTP request to retrieve the weather data
|
||||||
|
httpRequest( url, function( data ) {
|
||||||
|
try {
|
||||||
|
data = JSON.parse( data );
|
||||||
|
|
||||||
|
var tzOffset = getTimezone( data.current_observation.local_tz_offset, "minutes" ),
|
||||||
|
|
||||||
|
// Calculate sunrise and sunset since Weather Underground does not provide it
|
||||||
|
sunData = SunCalc.getTimes( data.current_observation.local_epoch * 1000,
|
||||||
|
data.current_observation.observation_location.latitude || data.current_observation.display_location.latitude,
|
||||||
|
data.current_observation.observation_location.longitude || data.current_observation.display_location.longitude );
|
||||||
|
|
||||||
|
sunData.sunrise.setUTCMinutes( sunData.sunrise.getUTCMinutes() + tzOffset );
|
||||||
|
sunData.sunset.setUTCMinutes( sunData.sunset.getUTCMinutes() + tzOffset );
|
||||||
|
|
||||||
|
var weather = {
|
||||||
|
icon: data.current_observation.icon,
|
||||||
|
timezone: data.current_observation.local_tz_offset,
|
||||||
|
sunrise: ( sunData.sunrise.getUTCHours() * 60 + sunData.sunrise.getUTCMinutes() ),
|
||||||
|
sunset: ( sunData.sunset.getUTCHours() * 60 + sunData.sunset.getUTCMinutes() ),
|
||||||
|
maxTemp: parseInt( data.history.dailysummary[0].maxtempi ),
|
||||||
|
minTemp: parseInt( data.history.dailysummary[0].mintempi ),
|
||||||
|
temp: parseInt( data.current_observation.temp_f ),
|
||||||
|
humidity: ( parseInt( data.history.dailysummary[0].maxhumidity ) + parseInt( data.history.dailysummary[0].minhumidity ) ) / 2,
|
||||||
|
precip: parseInt( data.current_observation.precip_today_in ) + parseInt( data.history.dailysummary[0].precipi ),
|
||||||
|
solar: parseInt( data.current_observation.UV ),
|
||||||
|
wind: parseInt( data.history.dailysummary[0].meanwindspdi ),
|
||||||
|
elevation: parseInt( data.current_observation.observation_location.elevation )
|
||||||
|
};
|
||||||
|
|
||||||
|
callback( weather );
|
||||||
|
|
||||||
|
} catch ( err ) {
|
||||||
|
|
||||||
|
// Otherwise indicate the request failed
|
||||||
|
callback( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve weather data to complete the weather request using The Weather Channel
|
||||||
|
function getWeatherData( location, callback ) {
|
||||||
|
|
||||||
|
// 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 ) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = JSON.parse( data );
|
||||||
|
|
||||||
|
var weather = {
|
||||||
|
iconCode: data.observation.icon_code,
|
||||||
|
timezone: data.observation.obs_time_local,
|
||||||
|
sunrise: parseDayTime( data.observation.sunrise ),
|
||||||
|
sunset: parseDayTime( data.observation.sunset ),
|
||||||
|
maxTemp: data.observation.imperial.temp_max_24hour,
|
||||||
|
minTemp: data.observation.imperial.temp_min_24hour,
|
||||||
|
temp: data.observation.imperial.temp,
|
||||||
|
humidity: data.observation.imperial.rh || 0,
|
||||||
|
precip: data.observation.imperial.precip_2day || data.observation.imperial.precip_24hour,
|
||||||
|
solar: data.observation.imperial.uv_index,
|
||||||
|
wind: data.observation.imperial.wspd
|
||||||
|
};
|
||||||
|
|
||||||
|
location = location.join( "," );
|
||||||
|
|
||||||
|
Cache.findOne( { location: location }, function( err, record ) {
|
||||||
|
|
||||||
|
if ( record && record.yesterdayHumidity !== null ) {
|
||||||
|
weather.yesterdayHumidity = record.yesterdayHumidity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the data to the callback function if successful
|
||||||
|
callback( weather );
|
||||||
|
} );
|
||||||
|
|
||||||
|
updateCache( location, weather );
|
||||||
|
} catch ( err ) {
|
||||||
|
|
||||||
|
// Otherwise indicate the request failed
|
||||||
|
callback( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the historical weather data for the provided location
|
||||||
|
function getYesterdayWeatherData( location, callback ) {
|
||||||
|
|
||||||
|
// Get the API key from the environment variables
|
||||||
|
var WSI_HISTORY_KEY = process.env.WSI_HISTORY_KEY,
|
||||||
|
|
||||||
|
// Generate a Date object for the previous day by subtracting a day (in milliseconds) from today
|
||||||
|
yesterday = toUSDate( new Date( new Date().getTime() - 1000 * 60 * 60 * 24 ) ),
|
||||||
|
|
||||||
|
// Generate URL using WSI Cleaned History API in Imperial units showing daily average values
|
||||||
|
url = "http://cleanedobservations.wsi.com/CleanedObs.svc/GetObs?ID=" + WSI_HISTORY_KEY +
|
||||||
|
"&Lat=" + location[0] + "&Long=" + location[1] +
|
||||||
|
"&Req=davg&startdate=" + yesterday + "&enddate=" + yesterday + "&TS=LST";
|
||||||
|
|
||||||
|
// Perform the HTTP request to retrieve the weather data
|
||||||
|
httpRequest( url, function( xml ) {
|
||||||
|
parseXML( xml, function( err, result ) {
|
||||||
|
callback( result.WeatherResponse.WeatherRecords[0].WeatherData[0].$ );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update weather cache record in the local database
|
||||||
|
function updateCache( location, weather ) {
|
||||||
|
|
||||||
|
// Search for a cache record for the provided location
|
||||||
|
Cache.findOne( { location: location }, function( err, record ) {
|
||||||
|
|
||||||
|
// If a record is found update the data and save it
|
||||||
|
if ( record ) {
|
||||||
|
|
||||||
|
record.currentHumidityTotal += weather.humidity;
|
||||||
|
record.currentHumidityCount++;
|
||||||
|
record.save();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// If no cache record is found, generate a new one and save it
|
||||||
|
new Cache( {
|
||||||
|
location: location,
|
||||||
|
currentHumidityTotal: weather.humidity,
|
||||||
|
currentHumidityCount: 1
|
||||||
|
} ).save();
|
||||||
|
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates the resulting water scale using the provided weather data, adjustment method and options
|
||||||
|
function calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ) {
|
||||||
|
|
||||||
|
// Zimmerman method
|
||||||
|
if ( adjustmentMethod === 1 ) {
|
||||||
|
|
||||||
|
// Check to make sure valid data exists for all factors
|
||||||
|
if ( !validateValues( [ "temp", "humidity", "precip" ], weather ) ) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var temp = ( ( weather.maxTemp + weather.minTemp ) / 2 ) || weather.temp,
|
||||||
|
humidityFactor = ( 30 - weather.humidity ),
|
||||||
|
tempFactor = ( ( temp - 70 ) * 4 ),
|
||||||
|
precipFactor = ( weather.precip * -200 );
|
||||||
|
|
||||||
|
// Apply adjustment options, if provided, by multiplying the percentage against the factor
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply all of the weather modifying factors and clamp the result between 0 and 200%.
|
||||||
|
return parseInt( Math.min( Math.max( 0, 100 + humidityFactor + tempFactor + precipFactor ), 200 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the weather data meets any of the restrictions set by OpenSprinkler.
|
||||||
|
// Restrictions prevent any watering from occurring and are similar to 0% watering level.
|
||||||
|
//
|
||||||
|
// All queries will return a restrict flag if the current weather indicates rain.
|
||||||
|
//
|
||||||
|
// California watering restriction prevents watering if precipitation over two days is greater
|
||||||
|
// than 0.01" over the past 48 hours.
|
||||||
|
function checkWeatherRestriction( adjustmentValue, weather ) {
|
||||||
|
|
||||||
|
// Define all the weather codes that indicate rain
|
||||||
|
var adverseCodes = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 35, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47 ],
|
||||||
|
adverseWords = [ "flurries", "sleet", "rain", "sleet", "snow", "tstorms" ];
|
||||||
|
|
||||||
|
if ( ( weather.iconCode && adverseCodes.indexOf( weather.iconCode ) !== -1 ) || ( weather.icon && adverseWords.indexOf( weather.icon ) !== -1 ) ) {
|
||||||
|
|
||||||
|
// If the current weather indicates rain, add a restrict flag to the weather script indicating
|
||||||
|
// the controller should not water.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var californiaRestriction = ( adjustmentValue >> 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.precip > 0.01 ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 ) {
|
||||||
|
|
||||||
|
// The adjustment method is encoded by the OpenSprinkler firmware and must be
|
||||||
|
// parsed. This allows the adjustment method and the restriction type to both
|
||||||
|
// be saved in the same byte.
|
||||||
|
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" ] || req.connection.remoteAddress,
|
||||||
|
|
||||||
|
// Function that will accept the weather after it is received from the API
|
||||||
|
// Data will be processed to retrieve the resulting scale, sunrise/sunset, timezone,
|
||||||
|
// and also calculate if a restriction is met to prevent watering.
|
||||||
|
finishRequest = function( weather ) {
|
||||||
|
if ( !weather ) {
|
||||||
|
res.send( "Error: No weather data found." );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
scale: calculateWeatherScale( adjustmentMethod, adjustmentOptions, weather ),
|
||||||
|
restrict: checkWeatherRestriction( req.params[0], weather ) ? 1 : 0,
|
||||||
|
tz: getTimezone( weather.timezone ),
|
||||||
|
sunrise: weather.sunrise,
|
||||||
|
sunset: weather.sunset,
|
||||||
|
eip: ipToInt( remoteAddress )
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return the response to the client in the requested format
|
||||||
|
if ( outputFormat === "json" ) {
|
||||||
|
res.json( data );
|
||||||
|
} else {
|
||||||
|
res.send( "&scale=" + data.scale +
|
||||||
|
"&restrict=" + data.restrict +
|
||||||
|
"&tz=" + data.tz +
|
||||||
|
"&sunrise=" + data.sunrise +
|
||||||
|
"&sunset=" + data.sunset +
|
||||||
|
"&eip=" + data.eip
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Exit if no location is provided
|
||||||
|
if ( !location ) {
|
||||||
|
res.send( "Error: No location provided." );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// X-Forwarded-For header may contain more than one IP address and therefore
|
||||||
|
// the string is split against a comma and the first value is selected
|
||||||
|
remoteAddress = remoteAddress.split( "," )[0];
|
||||||
|
|
||||||
|
// Parse weather adjustment options
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Reconstruct JSON string from deformed controller output
|
||||||
|
adjustmentOptions = JSON.parse( "{" + adjustmentOptions + "}" );
|
||||||
|
} catch ( err ) {
|
||||||
|
|
||||||
|
// If the JSON is not valid, do not incorporate weather adjustment options
|
||||||
|
adjustmentOptions = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse location string
|
||||||
|
if ( filters.pws.test( location ) ) {
|
||||||
|
|
||||||
|
// Handle locations using PWS or ICAO (Weather Underground)
|
||||||
|
if ( !weatherUndergroundKey ) {
|
||||||
|
|
||||||
|
// If no key is provided for Weather Underground then the PWS or ICAO cannot be resolved
|
||||||
|
res.send( "Error: Weather Underground key required when using PWS or ICAO location." );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPWSCoordinates( location, weatherUndergroundKey, function( result ) {
|
||||||
|
if ( result === false ) {
|
||||||
|
res.send( "Error: Unable to resolve location" );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = result;
|
||||||
|
getWeatherData( location, finishRequest );
|
||||||
|
} );
|
||||||
|
} else if ( weatherUndergroundKey ) {
|
||||||
|
|
||||||
|
// The current weather script uses Weather Underground and during the transition period
|
||||||
|
// both will be supported and users who provide a Weather Underground API key will continue
|
||||||
|
// using Weather Underground until The Weather Service becomes the default API
|
||||||
|
|
||||||
|
getWeatherUndergroundData( location, weatherUndergroundKey, finishRequest );
|
||||||
|
} else if ( filters.gps.test( location ) ) {
|
||||||
|
|
||||||
|
// Handle GPS coordinates by storing each coordinate in an array
|
||||||
|
location = location.split( "," );
|
||||||
|
location = [ parseFloat( location[0] ), parseFloat( location[1] ) ];
|
||||||
|
|
||||||
|
// Continue with the weather request
|
||||||
|
getWeatherData( location, finishRequest );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Attempt to resolve provided location to GPS coordinates when it does not match
|
||||||
|
// a GPS coordinate or Weather Underground location using Weather Underground autocomplete
|
||||||
|
resolveCoordinates( location, function( result ) {
|
||||||
|
if ( result === false ) {
|
||||||
|
res.send( "Error: Unable to resolve location" );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = result;
|
||||||
|
getWeatherData( location, finishRequest );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks to make sure an array contains the keys provided and returns true or false
|
||||||
|
function validateValues( keys, array ) {
|
||||||
|
var key;
|
||||||
|
|
||||||
|
for ( key in keys ) {
|
||||||
|
if ( !keys.hasOwnProperty( key ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
key = keys[key];
|
||||||
|
|
||||||
|
if ( !array.hasOwnProperty( key ) || typeof array[key] !== "number" || isNaN( array[key] ) || array[key] === null ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accepts a time string formatted in ISO-8601 or just the timezone
|
||||||
|
// offset and returns the timezone.
|
||||||
|
// The timezone output is formatted for OpenSprinkler Unified firmware.
|
||||||
|
function getTimezone( time, format ) {
|
||||||
|
|
||||||
|
// Match the provided time string against a regex for parsing
|
||||||
|
time = time.match( filters.time ) || time.match( filters.timezone );
|
||||||
|
|
||||||
|
var hour = parseInt( time[7] + time[8] ),
|
||||||
|
minute = parseInt( time[9] ),
|
||||||
|
tz;
|
||||||
|
|
||||||
|
if ( format === "minutes" ) {
|
||||||
|
tz = ( hour * 60 ) + minute;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Convert the timezone into the OpenSprinkler encoded format
|
||||||
|
minute = ( minute / 15 >> 0 ) / 4;
|
||||||
|
hour = hour + ( hour >= 0 ? minute : -minute );
|
||||||
|
|
||||||
|
tz = ( ( hour + 12 ) * 4 ) >> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tz;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to return the sunrise and sunset times from the weather reply
|
||||||
|
function parseDayTime( time ) {
|
||||||
|
|
||||||
|
// Time is parsed from string against a regex
|
||||||
|
time = time.match( filters.time );
|
||||||
|
|
||||||
|
// Values are converted to minutes from midnight for the controller
|
||||||
|
return parseInt( time[4] ) * 60 + parseInt( time[5] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts IP string to integer
|
||||||
|
function ipToInt( ip ) {
|
||||||
|
ip = ip.split( "." );
|
||||||
|
return ( ( ( ( ( ( +ip[0] ) * 256 ) + ( +ip[1] ) ) * 256 ) + ( +ip[2] ) ) * 256 ) + ( +ip[3] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolves the Month / Day / Year of a Date object
|
||||||
|
function toUSDate( date ) {
|
||||||
|
return ( date.getMonth() + 1 ) + "/" + date.getDate() + "/" + date.getFullYear();
|
||||||
|
}
|
||||||
62
server.js
Normal file
62
server.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
var express = require( "express" ),
|
||||||
|
weather = require( "./routes/weather.js" ),
|
||||||
|
mongoose = require( "mongoose" ),
|
||||||
|
Cache = require( "./models/Cache" ),
|
||||||
|
CronJob = require( "cron" ).CronJob,
|
||||||
|
port = process.env.PORT || 3000,
|
||||||
|
app = express();
|
||||||
|
|
||||||
|
if ( !process.env.PORT ) {
|
||||||
|
require( "dotenv" ).load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to local MongoDB instance
|
||||||
|
mongoose.connect( "localhost" );
|
||||||
|
|
||||||
|
// If the database connection cannot be established, throw an error
|
||||||
|
mongoose.connection.on( "error", function() {
|
||||||
|
console.error( "MongoDB Connection Error. Please make sure that MongoDB is running." );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// 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( "Error: Request not found" );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Start listening on the service port
|
||||||
|
app.listen( port, "127.0.0.1", function() {
|
||||||
|
|
||||||
|
console.log( "OpenSprinkler Weather Service now listening on port %s", port );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Schedule a cronjob daily to consildate the weather cache data, runs daily
|
||||||
|
new CronJob( "0 0 0 * * *", function() {
|
||||||
|
|
||||||
|
// Find all records in the weather cache
|
||||||
|
Cache.find( {}, function( err, records ) {
|
||||||
|
|
||||||
|
// Cycle through each record
|
||||||
|
records.forEach( function( record ){
|
||||||
|
|
||||||
|
// If the record contains any unaveraged data, then process the record
|
||||||
|
if ( record.currentHumidityCount > 0 ) {
|
||||||
|
|
||||||
|
// Average the humidity by dividing the total over the total data points collected
|
||||||
|
record.yesterdayHumidity = record.currentHumidityTotal / record.currentHumidityCount;
|
||||||
|
|
||||||
|
// Reset the current humidity data for the new day
|
||||||
|
record.currentHumidityTotal = 0;
|
||||||
|
record.currentHumidityCount = 0;
|
||||||
|
|
||||||
|
// Save the record in the database
|
||||||
|
record.save();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}, null, true, "UTC" );
|
||||||
Reference in New Issue
Block a user