#!/usr/bin/env python # # PyWeather example script for reading PyWeather stations uploading to one or # more PyWeather publication sites # # Author: Patrick C. McGinty # Email: pyweather@tuxcoder.com # Date: Sunday, May 02 2010 ''' Periodically read data from a local weather station and upload to the PyWeather publication site. ''' import os import sys import time import logging import optparse import configparser import weather.stations import weather.stations.netatmo import weather.services log = logging.getLogger('') # Intervals (in minutes) between each archive record generated by the weather # station: ARCHIVE_INTERVAL = 10 # Gust 'time to live'; define many minutes should gust be reported: GUST_TTL = 10 GUST_MPH_MIN = 7 # minimum mph of gust above avg wind speed to report # Publication Services Lookup Table # key expected to match optparse destination parameter # value defines class object of publication service PUB_SERVICES = { 'wug': weather.services.Wunderground, 'pws': weather.services.PwsWeather, 'pwsweather': weather.services.PwsWeather, 'file': weather.services.TextFile, } STATIONS = { 'netatmo': weather.stations.netatmo.NetatmoStation, 'vantage_pro': weather.stations.VantagePro, } class NoSensorException(Exception): pass class WindGust(object): NO_VALUE = ('NA', 'NA') def __init__(self): self.value = self.NO_VALUE self.count = 0 def get(self, station, interval): ''' return gust data, if above threshold value and current time is inside reporting window period ''' rec = station.fields['Archive'] # process new data if rec: threshold = station.fields['WindSpeed10Min'] + GUST_MPH_MIN if rec['WindHi'] >= threshold: self.value = (rec['WindHi'], rec['WindHiDir']) self.count = GUST_TTL * 60 / interval else: self.value = self.NO_VALUE # return gust value, if remaining time is left, and valid if self.count: self.count -= 1 else: self.value = self.NO_VALUE log.debug('wind gust of {0} mph from {1}'.format(*self.value)) return self.value WindGust = WindGust() def weather_update(station, pub_sites, interval): ''' main execution loop. query weather data and post to online service. ''' point = station.get_reading() # santity check weather data if point.temperature_f > 200: raise NoSensorException( 'Out of range temperature value: %.1f, check sensors' % (point.temperature_f,)) gust = None gust_dir = None if isinstance(station, weather.stations.VantagePro): # Wind is only supported in VantagePro. gust, gust_dir = WindGust.get(station, interval) # upload data in the following order: for ps in pub_sites: try: # try block necessary to attempt every publisher ps.set( pressure=point.pressure, dewpoint=point.dew_point_f, humidity=point.humidity, tempf=point.temperature_f, rainin=point.rain_rate_in, rainday=point.rain_day_in, windspeed=point.wind_speed_mph, winddir=point.wind_direction, windgust=gust, windgustdir=gust_dir, dateutc=point.time.strftime("%Y-%m-%d %H:%M:%S")) ps.publish() # TODO: add user-friendly name log.info("Published to %s", ps.__class__) except (Exception) as e: log.exception('publisher %s: %s' % (ps.__class__.__name__, e)) def init_log(quiet, debug): ''' setup system logging to desired verbosity. ''' from logging.handlers import SysLogHandler fmt = logging.Formatter( os.path.basename(sys.argv[0]) + ".%(name)s %(levelname)s - %(message)s") facility = SysLogHandler.LOG_DAEMON syslog = SysLogHandler(address='/dev/log', facility=facility) syslog.setFormatter(fmt) log.addHandler(syslog) if not quiet: console = logging.StreamHandler() console.setFormatter(fmt) log.addHandler(console) log.setLevel(logging.INFO) if debug: log.setLevel(logging.DEBUG) def get_pub_services(opts, config): ''' use values in opts data to generate instances of publication services. ''' sites = [] for p_key in list(vars(opts).keys()): args = getattr(opts, p_key) if p_key in PUB_SERVICES and args: if isinstance(args, tuple): ps = PUB_SERVICES[p_key](*args) else: ps = PUB_SERVICES[p_key](args) sites.append(ps) if config: for p_key in config['general']['publication'].split(','): ps = PUB_SERVICES[p_key](**config[p_key]) sites.append(ps) return sites def get_options(parser): ''' read command line options to configure program behavior. ''' # station services # publication services pub_g = optparse.OptionGroup( parser, "Publication Services", 'One or more publication service must be specified to enable upload ' 'of weather data.',) pub_g.add_option( '-w', '--wundergound', nargs=2, type='string', dest='wug', help='Weather Underground service; WUG=[SID(station ID), PASSWORD]') pub_g.add_option( '-p', '--pws', nargs=2, type='string', dest='pws', help='PWS service; PWS=[SID(station ID), PASSWORD]') pub_g.add_option( '-f', '--file', nargs=1, type='string', dest='file', help='Local file; FILE=[FILE_NAME]') parser.add_option_group(pub_g) parser.add_option( '-d', '--debug', dest='debug', action="store_true", default=False, help='enable verbose debug logging') parser.add_option( '-q', '--quiet', dest='quiet', action="store_true", default=False, help='disable all console logging') parser.add_option( '-t', '--tty', dest='tty', default='/dev/ttyS0', help='set serial port device [/dev/ttyS0]') parser.add_option( '-n', '--interval', dest='interval', default=60, type='int', help='polling/update interval in seconds [60]') parser.add_option('-c', '--config', dest='config_path', default=None, type='str', help='path to the configuration file') return parser.parse_args() if __name__ == '__main__': parser = optparse.OptionParser() opts, args = get_options(parser) init_log(opts.quiet, opts.debug) config = None if opts.config_path: config = configparser.ConfigParser() config.read_file(open(opts.config_path)) # configure publication service defined in command-line args pub_sites = get_pub_services(opts, config) if not pub_sites: log.error('no publication service defined') sys.exit(-1) if config: station_name = config['general']['station'] station = STATIONS[station_name](**config[station_name]) else: # Only VantagePro is supported without config. station = weather.stations.VantagePro(opts.tty, ARCHIVE_INTERVAL) while True: try: weather_update(station, pub_sites, opts.interval) except (Exception) as e: log.exception(e) # pause until next update time next_update = opts.interval - (time.time() % opts.interval) log.info('sleep') time.sleep(next_update)