2 # -*- coding: utf-8 -*-
5 InfoEx <-> NRCS Auto Wx implementation
7 Wylark Mountaineering LLC
11 This program fetches data from an NRCS SNOTEL site and pushes it to
12 InfoEx using the new automated weather system implementation.
14 It is designed to be run hourly, and it asks for the last three hours
15 of data of each desired type, and selects the most recent one. This
16 lends some resiliency to the process and helps ensure that we have a
17 value to send, but it can lead to somewhat inconsistent/untruthful
18 data if e.g. the HS is from the last hour but the tempPres is from two
19 hours ago because the instrumentation had a hiccup. It's worth
20 considering if this is a bug or a feature.
22 For more information, see file: README
23 For licensing, see file: LICENSE
32 from collections
import OrderedDict
33 from ftplib
import FTP
34 from optparse
import OptionParser
38 import zeep
.transports
40 log
= logging
.getLogger(__name__
)
41 log
.setLevel(logging
.DEBUG
)
44 from systemd
.journal
import JournalHandler
45 log
.addHandler(JournalHandler())
48 import logging
.handlers
49 log
.addHandler(logging
.handlers
.SysLogHandler())
51 parser
= OptionParser()
52 parser
.add_option("--config", dest
="config", metavar
="FILE", help="location of config file")
54 (options
, args
) = parser
.parse_args()
56 config
= configparser
.ConfigParser(allow_no_value
=False)
57 config
.read(options
.config
)
59 log
.debug('STARTING UP')
61 wsdl
= 'https://www.wcc.nrcs.usda.gov/awdbWebService/services?WSDL'
65 'host': config
['ftp']['host'],
66 'uuid': config
['ftp']['uuid'],
67 'api_key': config
['ftp']['api_key'],
68 'location_uuid': config
['wxsite']['location_uuid'],
69 'wx_data': {}, # placeholder key, values to come later
70 'csv_filename': config
['wxsite']['csv_filename']
73 station_triplet
= config
['wxsite']['station_triplet']
76 desired_data
= config
['wxsite']['desired_data'].split(',')
78 # desired_data malformed or missing, setting default
80 'TOBS', # AIR TEMPERATURE OBSERVED (degF)
81 'SNWD', # SNOW DEPTH (in)
82 'PREC' # PRECIPITATION ACCUMULATION (in)
85 log
.critical("%s not defined in %s" % (e
, options
.config
))
87 except Exception as exc
:
88 log
.critical("Exception occurred in config parsing: '%s'" % (exc
))
91 # all sections/values present in config file, final sanity check
93 for key
in config
.sections():
94 for subkey
in config
[key
]:
95 if not len(config
[key
][subkey
]):
97 except ValueError as exc
:
98 log
.critical("Config value '%s.%s' is empty" % (key
, subkey
))
103 # This won't earn style points in Python, but here we establish a couple
104 # of helpful mappings variables. The reason this is helpful is that the
105 # end result is simply an ordered set, the CSV file. But we still may
106 # want to manipulate the values arbitrarily before writing that file.
108 # Also note that the current Auto Wx InfoEx documentation shows these
109 # keys in a graphical table with the "index" beginning at 1, but here we
110 # are sanely indexing beginning at 0.
111 fmap
= {} ; final_data
= [None] * 29
112 fmap
['Location UUID'] = 0 ; final_data
[0] = infoex
['location_uuid']
113 fmap
['obDate'] = 1 ; final_data
[1] = None
114 fmap
['obTime'] = 2 ; final_data
[2] = None
115 fmap
['timeZone'] = 3 ; final_data
[3] = 'Pacific'
116 fmap
['tempMaxHour'] = 4 ; final_data
[4] = None
117 fmap
['tempMaxHourUnit'] = 5 ; final_data
[5] = 'F'
118 fmap
['tempMinHour'] = 6 ; final_data
[6] = None
119 fmap
['tempMinHourUnit'] = 7 ; final_data
[7] = 'F'
120 fmap
['tempPres'] = 8 ; final_data
[8] = None
121 fmap
['tempPresUnit'] = 9 ; final_data
[9] = 'F'
122 fmap
['precipitationGauge'] = 10 ; final_data
[10] = None
123 fmap
['precipitationGaugeUnit'] = 11 ; final_data
[11] = 'in'
124 fmap
['windSpeedNum'] = 12 ; final_data
[12] = None
125 fmap
['windSpeedUnit'] = 13 ; final_data
[13] = 'mph'
126 fmap
['windDirectionNum'] = 14 ; final_data
[14] = None
127 fmap
['hS'] = 15 ; final_data
[15] = None
128 fmap
['hsUnit'] = 16 ; final_data
[16] = 'in'
129 fmap
['baro'] = 17 ; final_data
[17] = None
130 fmap
['baroUnit'] = 18 ; final_data
[18] = 'inHg'
131 fmap
['rH'] = 19 ; final_data
[19] = None
132 fmap
['windGustSpeedNum'] = 20 ; final_data
[20] = None
133 fmap
['windGustSpeedNumUnit'] = 21 ; final_data
[21] = 'mph'
134 fmap
['windGustDirNum'] = 22 ; final_data
[22] = None
135 fmap
['dewPoint'] = 23 ; final_data
[23] = None
136 fmap
['dewPointUnit'] = 24 ; final_data
[24] = 'F'
137 fmap
['hn24Auto'] = 25 ; final_data
[25] = None
138 fmap
['hn24AutoUnit'] = 26 ; final_data
[26] = 'in'
139 fmap
['hstAuto'] = 27 ; final_data
[27] = None
140 fmap
['hstAutoUnit'] = 28 ; final_data
[28] = 'in'
142 # one final mapping, the NRCS fields that this program supports to
143 # their InfoEx counterpart
145 iemap
['PREC'] = 'precipitationGauge'
146 iemap
['TOBS'] = 'tempPres'
149 # floor time to nearest hour
150 dt
= datetime
.datetime
.now()
151 end_date
= dt
- datetime
.timedelta(minutes
=dt
.minute
% 60,
153 microseconds
=dt
.microsecond
)
154 begin_date
= end_date
- datetime
.timedelta(hours
=3)
156 transport
= zeep
.transports
.Transport(cache
=zeep
.cache
.SqliteCache())
157 client
= zeep
.Client(wsdl
=wsdl
, transport
=transport
)
158 time_all_elements
= time
.time()
160 log
.debug("Getting %s data from %s to %s" % (str(desired_data
),
161 str(begin_date
), str(end_date
)))
163 for elementCd
in desired_data
:
164 time_element
= time
.time()
166 # get the last three hours of data for this elementCd
167 tmp
= client
.service
.getHourlyData(
168 stationTriplets
=[station_triplet
],
171 beginDate
=begin_date
,
174 log
.info("Time to get elementCd '%s': %.3f sec" % (elementCd
,
175 time
.time() - time_element
))
177 values
= tmp
[0]['values']
179 # sort and isolate the most recent
181 # NOTE: we do this because sometimes there are gaps in hourly data
182 # in NRCS; yes, we may end up with slightly inaccurate data,
183 # so perhaps this decision will be re-evaluated in the future
185 ordered
= sorted(values
, key
=lambda t
: t
['dateTime'], reverse
=True)
186 infoex
['wx_data'][elementCd
] = ordered
[0]['value']
188 infoex
['wx_data'][elementCd
] = None
190 log
.info("Time to get all elementCds : %.3f sec" % (time
.time() -
193 log
.debug("infoex[wx_data]: %s", str(infoex
['wx_data']))
195 # Now we only need to add in what we want to change thanks to that
196 # abomination of a variable declaration earlier
197 final_data
[fmap
['Location UUID']] = infoex
['location_uuid']
198 final_data
[fmap
['obDate']] = end_date
.strftime('%m/%d/%Y')
199 final_data
[fmap
['obTime']] = end_date
.strftime('%H:%M')
201 for elementCd
in infoex
['wx_data']:
202 if elementCd
not in iemap
:
203 log
.warning("BAD KEY wx_data['%s']" % (elementCd
))
206 # CONSIDER: Casting every value to Float() -- need to investigate if
207 # any possible elementCds we may want are any other data
210 # Another possibility is to query the API with
211 # getStationElements and temporarily store the
212 # storedUnitCd. But that's pretty network-intensive and
213 # may not even be worth it if there's only e.g. one or two
214 # exceptions to any otherwise uniformly Float value set.
215 final_data
[fmap
[iemap
[elementCd
]]] = infoex
['wx_data'][elementCd
]
217 log
.debug("final_data: %s" % (str(final_data
)))
219 with
open(infoex
['csv_filename'], 'w') as f
:
220 # The requirement is that empty values are represented in the CSV
221 # file as "", csv.QUOTE_NONNUMERIC achieves that
222 log
.debug("writing CSV file '%s'" % (infoex
['csv_filename']))
223 writer
= csv
.writer(f
, quoting
=csv
.QUOTE_NONNUMERIC
)
224 writer
.writerow(final_data
)
227 #with open(infoex['csv_filename'], 'rb') as f:
228 # log.debug("uploading FTP file '%s'" % (infoex['host']))
229 # ftp = FTP(infoex['host'], infoex['uuid'], infoex['api_key'])
230 # ftp.storlines('STOR ' + infoex['csv_filename'], f)