241 lines
7.4 KiB
Python
241 lines
7.4 KiB
Python
|
#!/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)
|