From 3d6c7d6da32880c64b592d797c46bd74fd015997 Mon Sep 17 00:00:00 2001 From: Samer Albahra Date: Wed, 1 Jul 2015 00:12:02 -0500 Subject: [PATCH] Initial version of Node.js rewrite --- .gitignore | 3 + .jscsrc | 3 + Gruntfile.js | 45 +++++ application.py | 446 ---------------------------------------------- package.json | 13 ++ requirements.txt | 2 - routes/weather.js | 254 ++++++++++++++++++++++++++ server.js | 21 +++ 8 files changed, 339 insertions(+), 448 deletions(-) create mode 100644 .gitignore create mode 100644 .jscsrc create mode 100644 Gruntfile.js delete mode 100644 application.py create mode 100644 package.json delete mode 100644 requirements.txt create mode 100644 routes/weather.js create mode 100644 server.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d725b83 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +.env +WeatherService.zip diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..a87f8f0 --- /dev/null +++ b/.jscsrc @@ -0,0 +1,3 @@ +{ + "maximumLineLength": 150 +} diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..bce0537 --- /dev/null +++ b/Gruntfile.js @@ -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" ] ); + +}; diff --git a/application.py b/application.py deleted file mode 100644 index a8355bb..0000000 --- a/application.py +++ /dev/null @@ -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] diff --git a/package.json b/package.json new file mode 100644 index 0000000..b415d89 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 92cb6af..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytz==2014.10 -ephem==3.7.5.3 \ No newline at end of file diff --git a/routes/weather.js b/routes/weather.js new file mode 100644 index 0000000..f84b754 --- /dev/null +++ b/routes/weather.js @@ -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(); + } ); + } +}; + diff --git a/server.js b/server.js new file mode 100644 index 0000000..630b117 --- /dev/null +++ b/server.js @@ -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 ); +} );