2 # -*- coding: utf-8 -*-
5 InfoEx <-> NRCS/MesoWest Auto Wx implementation
7 Wylark Mountaineering LLC
9 This program fetches data from either an NRCS SNOTEL site or MesoWest
10 weather station and pushes it to InfoEx using the new automated weather
11 system implementation.
13 It is designed to be run hourly, and it asks for the last three hours
14 of data of each desired type, and selects the most recent one. This
15 lends some resiliency to the process and helps ensure that we have a
16 value to send, but it can lead to somewhat inconsistent/untruthful
17 data if e.g. the HS is from the last hour but the tempPres is from two
18 hours ago because the instrumentation had a hiccup. It's worth
19 considering if this is a bug or a feature.
21 For more information, see file: README
22 For licensing, see file: LICENSE
35 from ftplib
import FTP
36 from argparse
import ArgumentParser
44 import zeep
.transports
48 LOG
= logging
.getLogger(__name__
)
49 LOG
.setLevel(logging
.NOTSET
)
51 urllib3
.disable_warnings()
54 """Return OptionParser for this program"""
55 parser
= ArgumentParser()
57 parser
.add_argument("--version",
61 parser
.add_argument("--config",
64 help="location of config file")
66 parser
.add_argument("--log-level",
69 help="set the log level (debug, info, warning)")
71 parser
.add_argument("--dry-run",
75 help="fetch data but don't upload to InfoEx")
79 def setup_config(config
):
80 """Setup config variable based on values specified in the ini file"""
83 'host': config
['infoex']['host'],
84 'uuid': config
['infoex']['uuid'],
85 'api_key': config
['infoex']['api_key'],
86 'csv_filename': config
['infoex']['csv_filename'],
87 'location_uuid': config
['infoex']['location_uuid'],
88 'wx_data': {}, # placeholder key, values to come later
92 station
['provider'] = config
['station']['type']
94 if station
['provider'] not in ['nrcs', 'mesowest', 'python']:
95 print("Please specify either nrcs or mesowest as the station type.")
98 if station
['provider'] == 'nrcs':
99 station
['source'] = 'https://www.wcc.nrcs.usda.gov/awdbWebService/services?WSDL'
100 station
['station_id'] = config
['station']['station_id']
101 station
['desired_data'] = config
['station']['desired_data'].split(',')
102 station
['units'] = config
['station']['units']
104 if station
['provider'] == 'mesowest':
105 station
['source'] = 'https://api.synopticdata.com/v2/stations/timeseries'
106 station
['station_id'] = config
['station']['station_id']
107 station
['units'] = config
['station']['units']
108 station
['desired_data'] = config
['station']['desired_data']
110 # construct full API URL (sans start/end time, added later)
111 station
['source'] = station
['source'] + '?token=' + \
112 config
['station']['token'] + \
113 '&within=60&units=' + station
['units'] + \
114 '&stid=' + station
['station_id'] + \
115 '&vars=' + station
['desired_data']
117 if station
['provider'] == 'python':
118 station
['path'] = config
['station']['path']
120 tz
= 'America/Los_Angeles'
122 if 'tz' in config
['station']:
123 tz
= config
['station']['tz']
126 station
['tz'] = pytz
.timezone(tz
)
127 except pytz
.exceptions
.UnknownTimeZoneError
:
128 LOG
.critical("%s is not a valid timezone", tz
)
131 # By default, fetch three hours of data
133 # If user wants hn24 or wind averaging, then
135 station
['num_hrs_to_fetch'] = 3
138 if 'hn24' in config
['station']:
139 if config
['station']['hn24'] not in ['true', 'false']:
140 raise ValueError("hn24 must be either 'true' or 'false'")
142 if config
['station']['hn24'] == "true":
143 station
['hn24'] = True
144 station
['num_hrs_to_fetch'] = 24
146 station
['hn24'] = False
149 station
['hn24'] = False
152 if 'wind_mode' in config
['station']:
153 if config
['station']['wind_mode'] not in ['normal', 'average']:
154 raise ValueError("wind_mode must be either 'normal' or 'average'")
156 station
['wind_mode'] = config
['station']['wind_mode']
158 if station
['wind_mode'] == "average":
159 station
['num_hrs_to_fetch'] = 24
162 station
['wind_mode'] = "normal"
164 except KeyError as err
:
165 LOG
.critical("%s not defined in configuration file", err
)
167 except ValueError as err
:
168 LOG
.critical("%s", err
)
171 # all sections/values present in config file, final sanity check
173 for key
in config
.sections():
174 for subkey
in config
[key
]:
175 if not config
[key
][subkey
]:
178 LOG
.critical("Config value '%s.%s' is empty", key
, subkey
)
181 return (infoex
, station
)
183 def setup_logging(log_level
):
184 """Setup our logging infrastructure"""
186 from systemd
.journal
import JournalHandler
187 LOG
.addHandler(JournalHandler())
189 ## fallback to syslog
190 #import logging.handlers
191 #LOG.addHandler(logging.handlers.SysLogHandler())
193 handler
= logging
.StreamHandler(sys
.stdout
)
194 formatter
= logging
.Formatter('%(asctime)s.%(msecs)03d '
195 '%(levelname)s %(module)s - '
196 '%(funcName)s: %(message)s',
198 handler
.setFormatter(formatter
)
199 LOG
.addHandler(handler
)
202 if log_level
in [None, 'debug', 'info', 'warning']:
203 if log_level
== 'debug':
204 LOG
.setLevel(logging
.DEBUG
)
205 elif log_level
== 'info':
206 LOG
.setLevel(logging
.INFO
)
207 elif log_level
== 'warning':
208 LOG
.setLevel(logging
.WARNING
)
210 LOG
.setLevel(logging
.NOTSET
)
217 """Main routine: sort through args, decide what to do, then do it"""
218 parser
= get_parser()
219 options
= parser
.parse_args()
221 config
= configparser
.ConfigParser(allow_no_value
=False)
223 if not options
.config
:
225 print("\nPlease specify a configuration file via --config.")
228 config
.read(options
.config
)
230 if not setup_logging(options
.log_level
):
232 print("\nPlease select an appropriate log level or remove the switch (--log-level).")
235 (infoex
, station
) = setup_config(config
)
237 LOG
.debug('Config parsed, starting up')
240 (fmap
, final_data
) = setup_infoex_fields_mapping(infoex
['location_uuid'])
241 iemap
= setup_infoex_counterparts_mapping(station
['provider'])
243 # override units if user selected metric
244 if station
['provider'] != 'python' and station
['units'] == 'metric':
245 final_data
= switch_units_to_metric(final_data
, fmap
)
247 (begin_date
, end_date
) = setup_time_values(station
)
249 if station
['provider'] == 'python':
250 LOG
.debug("Getting custom data from external Python program")
252 LOG
.debug("Getting %s data from %s to %s (%s)",
253 str(station
['desired_data']),
254 str(begin_date
), str(end_date
), end_date
.tzinfo
.zone
)
256 time_all_elements
= time
.time()
259 if station
['provider'] == 'nrcs':
260 infoex
['wx_data'] = get_nrcs_data(begin_date
, end_date
, station
)
261 elif station
['provider'] == 'mesowest':
262 infoex
['wx_data'] = get_mesowest_data(begin_date
, end_date
,
264 elif station
['provider'] == 'python':
266 spec
= importlib
.util
.spec_from_file_location('custom_wx',
268 mod
= importlib
.util
.module_from_spec(spec
)
269 spec
.loader
.exec_module(mod
)
273 infoex
['wx_data'] = mod
.get_custom_data()
275 if infoex
['wx_data'] is None:
276 infoex
['wx_data'] = []
277 except Exception as exc
:
278 LOG
.error("Python program for custom Wx data failed in "
279 "execution: %s", str(exc
))
282 LOG
.info("Successfully executed external Python program")
284 LOG
.error("Please upgrade to Python 3.3 or later")
286 except FileNotFoundError
:
287 LOG
.error("Specified Python program for custom Wx data "
290 except Exception as exc
:
291 LOG
.error("A problem was encountered when attempting to "
292 "load your custom Wx program: %s", str(exc
))
295 LOG
.info("Time taken to get all data : %.3f sec", time
.time() -
298 LOG
.debug("infoex[wx_data]: %s", str(infoex
['wx_data']))
301 final_end_date
= end_date
.astimezone(station
['tz'])
303 # Now we only need to add in what we want to change thanks to that
304 # abomination of a variable declaration earlier
305 final_data
[fmap
['Location UUID']] = infoex
['location_uuid']
306 final_data
[fmap
['obDate']] = final_end_date
.strftime('%m/%d/%Y')
307 final_data
[fmap
['obTime']] = final_end_date
.strftime('%H:%M')
308 final_data
[fmap
['timeZone']] = station
['tz'].zone
310 for element_cd
in infoex
['wx_data']:
311 if element_cd
not in iemap
:
312 LOG
.warning("BAD KEY wx_data['%s']", element_cd
)
315 if infoex
['wx_data'][element_cd
] is None:
318 # do the conversion before the rounding
319 if station
['provider'] == 'nrcs' and station
['units'] == 'metric':
320 infoex
['wx_data'][element_cd
] = convert_nrcs_units_to_metric(element_cd
, infoex
['wx_data'][element_cd
])
322 # Massage precision of certain values to fit InfoEx's
325 # 0 decimal places: relative humidity, wind speed, wind
326 # direction, wind gust, snow depth
327 # 1 decimal place: air temp, baro
328 # Avoid transforming None values
329 if element_cd
in ['wind_speed', 'WSPD', 'wind_direction',
330 'RHUM', 'relative_humidity', 'WDIR',
331 'wind_gust', 'SNWD', 'snow_depth',
333 infoex
['wx_data'][element_cd
] = round(infoex
['wx_data'][element_cd
])
334 elif element_cd
in ['TOBS', 'air_temp', 'PRES', 'pressure']:
335 infoex
['wx_data'][element_cd
] = round(infoex
['wx_data'][element_cd
], 1)
336 elif element_cd
in ['PREC', 'precip_accum']:
337 infoex
['wx_data'][element_cd
] = round(infoex
['wx_data'][element_cd
], 2)
339 # CONSIDER: Casting every value to Float() -- need to investigate if
340 # any possible elementCds we may want are any other data
343 # Another possibility is to query the API with
344 # getStationElements and temporarily store the
345 # storedUnitCd. But that's pretty network-intensive and
346 # may not even be worth it if there's only e.g. one or two
347 # exceptions to any otherwise uniformly Float value set.
348 final_data
[fmap
[iemap
[element_cd
]]] = infoex
['wx_data'][element_cd
]
350 LOG
.debug("final_data: %s", str(final_data
))
352 if infoex
['wx_data']:
353 if not write_local_csv(infoex
['csv_filename'], final_data
):
354 LOG
.warning('Could not write local CSV file: %s',
355 infoex
['csv_filename'])
358 if not options
.dry_run
:
359 upload_csv(infoex
['csv_filename'], infoex
)
364 # data structure operations
365 def setup_infoex_fields_mapping(location_uuid
):
367 Create a mapping of InfoEx fields to the local data's indexing scheme.
371 This won't earn style points in Python, but here we establish a couple
372 of helpful mappings variables. The reason this is helpful is that the
373 end result is simply an ordered set, the CSV file. But we still may
374 want to manipulate the values arbitrarily before writing that file.
376 Also note that the current Auto Wx InfoEx documentation shows these
377 keys in a graphical table with the "index" beginning at 1, but here we
378 sanely index beginning at 0.
380 # pylint: disable=too-many-statements,multiple-statements,bad-whitespace
381 fmap
= {} ; final_data
= [None] * 29
382 fmap
['Location UUID'] = 0 ; final_data
[0] = location_uuid
383 fmap
['obDate'] = 1 ; final_data
[1] = None
384 fmap
['obTime'] = 2 ; final_data
[2] = None
385 fmap
['timeZone'] = 3 ; final_data
[3] = 'Pacific'
386 fmap
['tempMaxHour'] = 4 ; final_data
[4] = None
387 fmap
['tempMaxHourUnit'] = 5 ; final_data
[5] = 'F'
388 fmap
['tempMinHour'] = 6 ; final_data
[6] = None
389 fmap
['tempMinHourUnit'] = 7 ; final_data
[7] = 'F'
390 fmap
['tempPres'] = 8 ; final_data
[8] = None
391 fmap
['tempPresUnit'] = 9 ; final_data
[9] = 'F'
392 fmap
['precipitationGauge'] = 10 ; final_data
[10] = None
393 fmap
['precipitationGaugeUnit'] = 11 ; final_data
[11] = 'in'
394 fmap
['windSpeedNum'] = 12 ; final_data
[12] = None
395 fmap
['windSpeedUnit'] = 13 ; final_data
[13] = 'mph'
396 fmap
['windDirectionNum'] = 14 ; final_data
[14] = None
397 fmap
['hS'] = 15 ; final_data
[15] = None
398 fmap
['hsUnit'] = 16 ; final_data
[16] = 'in'
399 fmap
['baro'] = 17 ; final_data
[17] = None
400 fmap
['baroUnit'] = 18 ; final_data
[18] = 'inHg'
401 fmap
['rH'] = 19 ; final_data
[19] = None
402 fmap
['windGustSpeedNum'] = 20 ; final_data
[20] = None
403 fmap
['windGustSpeedNumUnit'] = 21 ; final_data
[21] = 'mph'
404 fmap
['windGustDirNum'] = 22 ; final_data
[22] = None
405 fmap
['dewPoint'] = 23 ; final_data
[23] = None
406 fmap
['dewPointUnit'] = 24 ; final_data
[24] = 'F'
407 fmap
['hn24Auto'] = 25 ; final_data
[25] = None
408 fmap
['hn24AutoUnit'] = 26 ; final_data
[26] = 'in'
409 fmap
['hstAuto'] = 27 ; final_data
[27] = None
410 fmap
['hstAutoUnit'] = 28 ; final_data
[28] = 'in'
412 return (fmap
, final_data
)
414 def setup_infoex_counterparts_mapping(provider
):
416 Create a mapping of the NRCS/MesoWest fields that this program supports to
417 their InfoEx counterparts
421 if provider
== 'nrcs':
422 iemap
['PREC'] = 'precipitationGauge'
423 iemap
['TOBS'] = 'tempPres'
424 iemap
['TMAX'] = 'tempMaxHour'
425 iemap
['TMIN'] = 'tempMinHour'
427 iemap
['PRES'] = 'baro'
429 iemap
['WSPD'] = 'windSpeedNum'
430 iemap
['WDIR'] = 'windDirectionNum'
431 # unsupported by NRCS:
434 # NOTE: this doesn't exist in NRCS SNOTEL, we create it in this
435 # program, so add it to the map here
436 iemap
['hn24'] = 'hn24Auto'
437 elif provider
== 'mesowest':
438 iemap
['precip_accum'] = 'precipitationGauge'
439 iemap
['air_temp'] = 'tempPres'
440 iemap
['air_temp_high_24_hour'] = 'tempMaxHour'
441 iemap
['air_temp_low_24_hour'] = 'tempMinHour'
442 iemap
['snow_depth'] = 'hS'
443 iemap
['pressure'] = 'baro'
444 iemap
['relative_humidity'] = 'rH'
445 iemap
['wind_speed'] = 'windSpeedNum'
446 iemap
['wind_direction'] = 'windDirectionNum'
447 iemap
['wind_gust'] = 'windGustSpeedNum'
449 # NOTE: this doesn't exist in MesoWest, we create it in this
450 # program, so add it to the map here
451 iemap
['hn24'] = 'hn24Auto'
452 elif provider
== 'python':
453 # we expect Python programs to use the InfoEx data type names
454 iemap
['precipitationGauge'] = 'precipitationGauge'
455 iemap
['tempPres'] = 'tempPres'
456 iemap
['tempMaxHour'] = 'tempMaxHour'
457 iemap
['tempMinHour'] = 'tempMinHour'
459 iemap
['baro'] = 'baro'
461 iemap
['windSpeedNum'] = 'windSpeedNum'
462 iemap
['windDirectionNum'] = 'windDirectionNum'
463 iemap
['windGustSpeedNum'] = 'windGustSpeedNum'
467 # provider-specific operations
468 def get_nrcs_data(begin
, end
, station
):
469 """get the data we're after from the NRCS WSDL"""
470 transport
= zeep
.transports
.Transport(cache
=zeep
.cache
.SqliteCache())
471 transport
.session
.verify
= False
472 client
= zeep
.Client(wsdl
=station
['source'], transport
=transport
)
475 # massage begin/end date format
476 begin_date_str
= begin
.strftime('%Y-%m-%d %H:%M:00')
477 end_date_str
= end
.strftime('%Y-%m-%d %H:%M:00')
479 for element_cd
in station
['desired_data']:
480 time_element
= time
.time()
482 # get the last three hours of data for this elementCd/element_cd
483 tmp
= client
.service
.getHourlyData(
484 stationTriplets
=[station
['station_id']],
485 elementCd
=element_cd
,
487 beginDate
=begin_date_str
,
488 endDate
=end_date_str
)
490 LOG
.info("Time to get NRCS elementCd '%s': %.3f sec", element_cd
,
491 time
.time() - time_element
)
493 values
= tmp
[0]['values']
495 # sort and isolate the most recent
497 # NOTE: we do this because sometimes there are gaps in hourly data
498 # in NRCS; yes, we may end up with slightly inaccurate data,
499 # so perhaps this decision will be re-evaluated in the future
501 ordered
= sorted(values
, key
=lambda t
: t
['dateTime'], reverse
=True)
502 remote_data
[element_cd
] = ordered
[0]['value']
504 remote_data
[element_cd
] = None
507 # calc hn24, if applicable
513 if element_cd
== "SNWD":
514 for idx
, _
in enumerate(values
):
518 hn24_values
.append(val
['value'])
520 if len(hn24_values
) > 0:
521 # instead of taking MAX - MIN, we want the first
522 # value (most distant) - the last value (most
525 # if the result is positive, then we have
526 # settlement; if it's not, then we have HN24
527 hn24
= hn24_values
[0] - hn24_values
[len(hn24_values
)-1]
532 # this case represents HS settlement
535 # finally, if user wants hn24 and it's set to None at this
536 # point, then force it to 0.0
541 remote_data
['hn24'] = hn24
545 def get_mesowest_data(begin
, end
, station
):
546 """get the data we're after from the MesoWest/Synoptic API"""
549 # massage begin/end date format
550 begin_date_str
= begin
.strftime('%Y%m%d%H%M')
551 end_date_str
= end
.strftime('%Y%m%d%H%M')
553 # construct final, completed API URL
554 api_req_url
= station
['source'] + '&start=' + begin_date_str
+ '&end=' + end_date_str
557 req
= requests
.get(api_req_url
)
558 except requests
.exceptions
.ConnectionError
:
559 LOG
.error("Could not connect to '%s'", api_req_url
)
565 LOG
.error("Bad JSON in MesoWest response")
569 observations
= json
['STATION'][0]['OBSERVATIONS']
570 except KeyError as exc
:
571 LOG
.error("Unexpected JSON in MesoWest response: '%s'", exc
)
573 except IndexError as exc
:
574 LOG
.error("Unexpected JSON in MesoWest response: '%s'", exc
)
576 LOG
.error("Detailed MesoWest response: '%s'",
577 json
['SUMMARY']['RESPONSE_MESSAGE'])
581 except ValueError as exc
:
582 LOG
.error("Bad JSON in MesoWest response: '%s'", exc
)
585 # pos represents the last item in the array, aka the most recent
586 pos
= len(observations
['date_time']) - 1
588 # while these values only apply in certain cases, init them here
589 wind_speed_values
= []
590 wind_gust_speed_values
= []
591 wind_direction_values
= []
595 wind_speed_avg
= None
596 wind_gust_speed_avg
= None
597 wind_direction_avg
= None
600 for element_cd
in station
['desired_data'].split(','):
601 # sort and isolate the most recent, see note above in NRCS for how and
604 # NOTE: Unlike in the NRCS case, the MesoWest API response contains all
605 # data (whereas with NRCS, we have to make a separate request for
606 # each element we want). This is nice for network efficiency but
607 # it means we have to handle this part differently for each.
609 # NOTE: Also unlike NRCS, MesoWest provides more granular data; NRCS
610 # provides hourly data, but MesoWest can often provide data every
611 # 10 minutes -- though this provides more opportunity for
614 # we may not have the data at all
615 key_name
= element_cd
+ '_set_1'
617 if key_name
in observations
:
618 # val is what will make it into the dataset, after
619 # conversions... it gets defined here because in certain
620 # cases we need to look at all of the data to calculate HN24
621 # or wind averages, but for the rest of the data, we only
622 # take the most recent
625 # loop through all observations for this key_name
626 # record relevant values for wind averaging or hn24, but
627 # otherwise only persist the data if it's the last datum in
629 for idx
, _
in enumerate(observations
[key_name
]):
630 val
= observations
[key_name
][idx
]
636 # mesowest by default provides wind_speed in m/s, but
637 # we specify 'english' units in the request; either way,
639 if element_cd
in ('wind_speed', 'wind_gust'):
642 # mesowest provides HS in mm, not cm; we want cm
643 if element_cd
== 'snow_depth' and station
['units'] == 'metric':
646 # HN24 / wind_mode transformations, once the data has
647 # completed unit conversions
648 if station
['wind_mode'] == "average":
649 if element_cd
== 'wind_speed' and val
is not None:
650 wind_speed_values
.append(val
)
651 elif element_cd
== 'wind_gust' and val
is not None:
652 wind_gust_speed_values
.append(val
)
653 elif element_cd
== 'wind_direction' and val
is not None:
654 wind_direction_values
.append(val
)
656 if element_cd
== 'snow_depth':
657 hn24_values
.append(val
)
659 # again, only persist this datum to the final data if
660 # it's from the most recent date
662 remote_data
[element_cd
] = val
664 # ensure that the data is filled out
665 if not observations
[key_name
][pos
]:
666 remote_data
[element_cd
] = None
668 remote_data
[element_cd
] = None
670 if len(hn24_values
) > 0:
671 # instead of taking MAX - MIN, we want the first value (most
672 # distant) - the last value (most recent)
674 # if the result is positive, then we have settlement; if it's not,
676 hn24
= hn24_values
[0] - hn24_values
[len(hn24_values
)-1]
681 # this case represents HS settlement
685 # finally, if user wants hn24 and it's set to None at this
686 # point, then force it to 0.0
687 if station
['hn24'] and hn24
is None:
690 if len(wind_speed_values
) > 0:
691 wind_speed_avg
= sum(wind_speed_values
) / len(wind_speed_values
)
693 if len(wind_gust_speed_values
) > 0:
694 wind_gust_speed_avg
= sum(wind_gust_speed_values
) / len(wind_gust_speed_values
)
696 if len(wind_direction_values
) > 0:
697 wind_direction_avg
= sum(wind_direction_values
) / len(wind_direction_values
)
700 remote_data
['hn24'] = hn24
702 # overwrite the following with the respective averages, if
704 if wind_speed_avg
is not None:
705 remote_data
['wind_speed'] = wind_speed_avg
707 if wind_gust_speed_avg
is not None:
708 remote_data
['wind_gust'] = wind_gust_speed_avg
710 if wind_direction_avg
is not None:
711 remote_data
['wind_direction'] = wind_direction_avg
715 def switch_units_to_metric(data_map
, mapping
):
716 """replace units with metric counterparts"""
718 # NOTE: to update this, use the fmap<->final_data mapping laid out
719 # in setup_infoex_fields_mapping ()
720 data_map
[mapping
['tempMaxHourUnit']] = 'C'
721 data_map
[mapping
['tempMinHourUnit']] = 'C'
722 data_map
[mapping
['tempPresUnit']] = 'C'
723 data_map
[mapping
['precipitationGaugeUnit']] = 'mm'
724 data_map
[mapping
['hsUnit']] = 'cm'
725 data_map
[mapping
['windSpeedUnit']] = 'm/s'
726 data_map
[mapping
['windGustSpeedNumUnit']] = 'm/s'
727 data_map
[mapping
['dewPointUnit']] = 'C'
728 data_map
[mapping
['hn24AutoUnit']] = 'cm'
729 data_map
[mapping
['hstAutoUnit']] = 'cm'
733 def convert_nrcs_units_to_metric(element_cd
, value
):
734 """convert NRCS values from English to metric"""
735 if element_cd
== 'TOBS':
736 value
= f_to_c(value
)
737 elif element_cd
== 'SNWD':
738 value
= in_to_cm(value
)
739 elif element_cd
== 'PREC':
740 value
= in_to_mm(value
)
744 def write_local_csv(path_to_file
, data
):
745 """Write the specified CSV file to disk"""
746 with
open(path_to_file
, 'w') as file_object
:
747 # The requirement is that empty values are represented in the CSV
748 # file as "", csv.QUOTE_NONNUMERIC achieves that
749 LOG
.debug("writing CSV file '%s'", path_to_file
)
750 writer
= csv
.writer(file_object
, quoting
=csv
.QUOTE_NONNUMERIC
)
751 writer
.writerow(data
)
755 def upload_csv(path_to_file
, infoex_data
):
756 """Upload the specified CSV file to InfoEx FTP and remove the file"""
757 with
open(path_to_file
, 'rb') as file_object
:
758 LOG
.debug("uploading FTP file '%s'", infoex_data
['host'])
759 ftp
= FTP(infoex_data
['host'], infoex_data
['uuid'],
760 infoex_data
['api_key'])
761 ftp
.storlines('STOR ' + path_to_file
, file_object
)
764 os
.remove(path_to_file
)
766 # other miscellaneous routines
767 def setup_time_values(station
):
768 """establish time bounds of data request(s)"""
770 # default timezone to UTC (for MesoWest)
773 # but for NRCS, use the config-specified timezone
774 if station
['provider'] == 'nrcs':
777 # floor time to nearest hour
778 date_time
= datetime
.datetime
.now(tz
=tz
)
779 end_date
= date_time
- datetime
.timedelta(minutes
=date_time
.minute
% 60,
780 seconds
=date_time
.second
,
781 microseconds
=date_time
.microsecond
)
782 begin_date
= end_date
- datetime
.timedelta(hours
=station
['num_hrs_to_fetch'])
783 return (begin_date
, end_date
)
786 """convert Fahrenheit to Celsius"""
787 return (float(f
) - 32) * 5.0/9.0
789 def in_to_cm(inches
):
790 """convert inches to centimetrs"""
791 return float(inches
) * 2.54
793 def in_to_mm(inches
):
794 """convert inches to millimeters"""
795 return (float(inches
) * 2.54) * 10.0
798 """convert meters per second to miles per hour"""
802 """convert knots to miles per hour"""
806 """convert millimeters to centimetrs"""
809 if __name__
== "__main__":