Merge branch 'release-3.0.1' into develop
[infoex-autowx.git] / infoex-autowx.py
index 79e0fd3064a41d477811a4c5311b9977d9aefca9..d58baac635453d4fd06bd00f23a6e2ffc2d1620b 100755 (executable)
@@ -39,7 +39,7 @@ import zeep
 import zeep.cache
 import zeep.transports
 
-__version__ = '2.0.1'
+__version__ = '3.0.1'
 
 LOG = logging.getLogger(__name__)
 LOG.setLevel(logging.NOTSET)
@@ -85,7 +85,7 @@ def setup_config(config):
         station = dict()
         station['provider'] = config['station']['type']
 
-        if station['provider'] not in ['nrcs', 'mesowest']:
+        if station['provider'] not in ['nrcs', 'mesowest', 'python']:
             print("Please specify either nrcs or mesowest as the station type.")
             sys.exit(1)
 
@@ -112,9 +112,12 @@ def setup_config(config):
                                 '&stid=' + station['station_id'] + \
                                 '&vars=' + station['desired_data']
 
+        if station['provider'] == 'python':
+            station['path'] = config['station']['path']
+
     except KeyError as err:
         LOG.critical("%s not defined in configuration file", err)
-        exit(1)
+        sys.exit(1)
 
     # all sections/values present in config file, final sanity check
     try:
@@ -124,7 +127,7 @@ def setup_config(config):
                     raise ValueError
     except ValueError:
         LOG.critical("Config value '%s.%s' is empty", key, subkey)
-        exit(1)
+        sys.exit(1)
 
     return (infoex, station)
 
@@ -184,14 +187,23 @@ def main():
     iemap = setup_infoex_counterparts_mapping(station['provider'])
 
     # override units if user selected metric
-    if station['units'] == 'metric':
-        final_data = switch_units_to_metric(final_data, fmap)
+    try:
+        if station['units'] == 'metric':
+            final_data = switch_units_to_metric(final_data, fmap)
+    except KeyError:
+        if station['provider'] != 'python':
+            LOG.error("Please specify the units in the configuration "
+                      "file")
+            sys.exit(1)
 
     (begin_date, end_date) = setup_time_values()
 
-    # get the data
-    LOG.debug("Getting %s data from %s to %s", str(station['desired_data']),
-              str(begin_date), str(end_date))
+    if station['provider'] == 'python':
+        LOG.debug("Getting custom data from external Python program")
+    else:
+        LOG.debug("Getting %s data from %s to %s",
+                  str(station['desired_data']),
+                  str(begin_date), str(end_date))
 
     time_all_elements = time.time()
 
@@ -201,6 +213,38 @@ def main():
     elif station['provider'] == 'mesowest':
         infoex['wx_data'] = get_mesowest_data(begin_date, end_date,
                                               station)
+    elif station['provider'] == 'python':
+        try:
+            import importlib.util
+
+            spec = importlib.util.spec_from_file_location('custom_wx',
+                                                          station['path'])
+            mod = importlib.util.module_from_spec(spec)
+            spec.loader.exec_module(mod)
+            mod.LOG = LOG
+
+            try:
+                infoex['wx_data'] = mod.get_custom_data()
+
+                if infoex['wx_data'] is None:
+                    infoex['wx_data'] = []
+            except Exception as exc:
+                LOG.error("Python program for custom Wx data failed in "
+                          "execution: %s", str(exc))
+                sys.exit(1)
+
+            LOG.info("Successfully executed external Python program")
+        except ImportError:
+            LOG.error("Please upgrade to Python 3.3 or later")
+            sys.exit(1)
+        except FileNotFoundError:
+            LOG.error("Specified Python program for custom Wx data "
+                      "was not found")
+            sys.exit(1)
+        except Exception as exc:
+            LOG.error("A problem was encountered when attempting to "
+                      "load your custom Wx program: %s", str(exc))
+            sys.exit(1)
 
     LOG.info("Time taken to get all data : %.3f sec", time.time() -
              time_all_elements)
@@ -218,6 +262,21 @@ def main():
             LOG.warning("BAD KEY wx_data['%s']", element_cd)
             continue
 
+        # Massage precision of certain values to fit InfoEx's
+        # expectations
+        #
+        # 0 decimal places: wind speed, wind direction, wind gust, snow depth
+        # 1 decimal place:  air temp, relative humidity, baro
+        # Avoid transforming None values
+        if infoex['wx_data'][element_cd] is None:
+            continue
+        elif element_cd in ['wind_speed', 'WSPD', 'wind_direction',
+                            'WDIR', 'wind_gust', 'SNWD', 'snow_depth']:
+            infoex['wx_data'][element_cd] = round(infoex['wx_data'][element_cd])
+        elif element_cd in ['TOBS', 'air_temp', 'RHUM',
+                            'relative_humidity', 'PRES', 'pressure']:
+            infoex['wx_data'][element_cd] = round(infoex['wx_data'][element_cd], 1)
+
         # CONSIDER: Casting every value to Float() -- need to investigate if
         #           any possible elementCds we may want are any other data
         #           type than float.
@@ -231,13 +290,14 @@ def main():
 
     LOG.debug("final_data: %s", str(final_data))
 
-    if not write_local_csv(infoex['csv_filename'], final_data):
-        LOG.warning('Could not write local CSV file: %s',
-                    infoex['csv_filename'])
-        return 1
+    if infoex['wx_data']:
+        if not write_local_csv(infoex['csv_filename'], final_data):
+            LOG.warning('Could not write local CSV file: %s',
+                        infoex['csv_filename'])
+            return 1
 
-    if not options.dry_run:
-        upload_csv(infoex['csv_filename'], infoex)
+        if not options.dry_run:
+            upload_csv(infoex['csv_filename'], infoex)
 
     LOG.debug('DONE')
     return 0
@@ -302,6 +362,8 @@ def setup_infoex_counterparts_mapping(provider):
     if provider == 'nrcs':
         iemap['PREC'] = 'precipitationGauge'
         iemap['TOBS'] = 'tempPres'
+        iemap['TMAX'] = 'tempMaxHour'
+        iemap['TMIN'] = 'tempMinHour'
         iemap['SNWD'] = 'hS'
         iemap['PRES'] = 'baro'
         iemap['RHUM'] = 'rH'
@@ -312,12 +374,26 @@ def setup_infoex_counterparts_mapping(provider):
     elif provider == 'mesowest':
         iemap['precip_accum'] = 'precipitationGauge'
         iemap['air_temp'] = 'tempPres'
+        iemap['air_temp_high_24_hour'] = 'tempMaxHour'
+        iemap['air_temp_low_24_hour'] = 'tempMinHour'
         iemap['snow_depth'] = 'hS'
         iemap['pressure'] = 'baro'
         iemap['relative_humidity'] = 'rH'
         iemap['wind_speed'] = 'windSpeedNum'
         iemap['wind_direction'] = 'windDirectionNum'
         iemap['wind_gust'] = 'windGustSpeedNum'
+    elif provider == 'python':
+        # we expect Python programs to use the InfoEx data type names
+        iemap['precipitationGauge'] = 'precipitationGauge'
+        iemap['tempPres'] = 'tempPres'
+        iemap['tempMaxHour'] = 'tempMaxHour'
+        iemap['tempMinHour'] = 'tempMinHour'
+        iemap['hS'] = 'hS'
+        iemap['baro'] = 'baro'
+        iemap['rH'] = 'rH'
+        iemap['windSpeedNum'] = 'windSpeedNum'
+        iemap['windDirectionNum'] = 'windDirectionNum'
+        iemap['windGustSpeedNum'] = 'windGustSpeedNum'
 
     return iemap
 
@@ -399,9 +475,14 @@ def get_mesowest_data(begin, end, station):
 
         # we may not have the data at all
         key_name = element_cd + '_set_1'
+
         if key_name in observations:
             if observations[key_name][pos]:
                 remote_data[element_cd] = observations[key_name][pos]
+
+                # mesowest provides wind_speed in m/s, we want mph
+                if element_cd in ('wind_speed', 'wind_gust'):
+                    remote_data[element_cd] = ms_to_mph(remote_data[element_cd])
             else:
                 remote_data[element_cd] = None
         else:
@@ -460,5 +541,9 @@ def setup_time_values():
     begin_date = end_date - datetime.timedelta(hours=3)
     return (begin_date, end_date)
 
+def ms_to_mph(ms):
+    """convert meters per second to miles per hour"""
+    return ms * 2.236936
+
 if __name__ == "__main__":
     sys.exit(main())