# # Copyright (c) 2013 Tom Keffer # # See the file LICENSE.txt for your full rights. # # $Revision$ # $Author$ # $Date$ """Special service for publishing to the Weather Underground's RapidFire protocol. This version is experimental and could change at any time --- don't get too attached to it! It only works with the weewx Davis Vantage driver. It also only works with databases that are in US Customary units. NOT Metric! To use: Add a section to your configuration file that looks very similar to the normal WeatherUnderground section, except it is a weewx service: [StdRapidFire] station = KORHOODR9 password = my_password Then add the StdRapidFire service to the list of services to be run: service_list = ... [services elided] ..., weewx.wxengine.StdRESTful, rapidfire.StdRapidFire, ... You may have to adjust your PYTHONPATH appropriately. E.g., export PYTHONPATH=/home/weewx/experimental """ import Queue import datetime import httplib import socket import syslog import threading import urllib import urllib2 import weewx.wxengine import weewx.restful class StdRapidFire(weewx.wxengine.StdService): """Adds the ability to post to the Weather Underground's RapidFire facility""" def __init__(self, engine, config_dict): super(StdRapidFire, self).__init__(engine, config_dict) self.bind(weewx.NEW_LOOP_PACKET, self.new_loop_packet) # Loop packets will be pushed on to this queue: self.rapidfire_queue = Queue.Queue() # Start up a thread for the RapidFire: self.rapidfire_thread = RapidFire(self.rapidfire_queue, **config_dict['StdRapidFire']) self.rapidfire_thread.start() syslog.syslog(syslog.LOG_DEBUG, "rapidfire: Started RapidFire thread.") def new_loop_packet(self, event): # Push the new packet on to the queue: self.rapidfire_queue.put(event.packet) def shutDown(self): """Shut down the RapidFire thread""" # Put a None in the queue. This will signal the thread to shutdown self.rapidfire_queue.put(None) # Wait up to 20 seconds for the thread to exit: self.rapidfire_thread.join(20.0) if self.rapidfire_thread.isAlive(): syslog.syslog(syslog.LOG_ERR, "rapidfire: Unable to shut down RapidFire thread") else: syslog.syslog(syslog.LOG_DEBUG, "rapidfire: Shut down RapidFire thread.") class RapidFire(threading.Thread): """Dedicated thread for publishing weather data using the WU RapidFire protocol Inherits from threading.Thread. Basically, it watches a queue, and if a packet appears in it, it publishes it. """ # Types and formats of the data to be published: _formats = {'dateTime' : 'dateutc=%s', 'barometer' : 'baromin=%.3f', 'outTemp' : 'tempf=%.1f', 'outHumidity' : 'humidity=%03.0f', 'windSpeed' : 'windspeedmph=%03.0f', 'windDir' : 'winddir=%.0f', 'windGust' : 'windgustmph=%03.0f', 'windGustDir' : 'windgustdir=%03.0f', 'dewpoint' : 'dewptf=%.1f', 'rainRate' : 'rainin=%.2f', 'dayRain' : 'dailyrainin=%.2f', 'radiation' : 'solarradiation=%.2f', 'UV' : 'UV=%.2f'} def __init__(self, rapidfire_queue, **rf_dict): threading.Thread.__init__(self, name="RapidFireThread") self.rapidfire_queue = rapidfire_queue self.station = rf_dict['station'] self.password = rf_dict['password'] self.timeout = int(rf_dict.get('timeout', 5)) # In the strange vocabulary of Python, declaring yourself a "daemon thread" # allows the program to exit even if this thread is running: self.setDaemon(True) def run(self): while True : # This will block until something appears in the queue: packet = self.rapidfire_queue.get() # A 'None' value appearing in the queue is our signal to exit if packet is None: break # If packets have backed up in the queue, throw them all away except the last one. while self.rapidfire_queue.qsize(): packet = self.rapidfire_queue.get() if packet is None: break # Form the URL from the information in the packet url = self.getURL(packet) # Now post it self.postURL(url) def getURL(self, packet): """Return an URL for posting using the RF protocol""" _liststr = ["action=updateraw", "ID=%s" % self.station, "PASSWORD=%s" % self.password ] # Go through each of the supported types, formatting it, then adding to _liststr: for _key in RapidFire._formats: v = packet.get(_key) # Check to make sure the type is not null if v is not None : if _key == 'dateTime': # For dates, convert from time stamp to a string, using what # the Weather Underground calls "MySQL format." I've fiddled # with formatting, and it seems that escaping the colons helps # its reliability. But, I could be imagining things. v = urllib.quote(datetime.datetime.utcfromtimestamp(v).isoformat('+'), '-+') # Format the value, and accumulate in _liststr: _liststr.append(RapidFire._formats[_key] % v) # Add the realtime flag ... _liststr.append("realtime=1") # ... and the update frequency ... _liststr.append("rtfreq=2.5") # ... and the software type and version: _liststr.append("softwaretype=weewx-%s" % weewx.__version__) # Now stick all the little pieces together with an ampersand between them: _urlquery='&'.join(_liststr) # This will be the complete URL for the HTTP GET: _url="http://rtupdate.wunderground.com/weatherstation/updateweatherstation.php?" + _urlquery return _url def postURL(self, url): """Post the given url to the Weather Underground's RapidFire site.""" try: _response = urllib2.urlopen(url, timeout=self.timeout) except (urllib2.URLError, socket.error, httplib.BadStatusLine), e: # Unsuccessful. Log it syslog.syslog(syslog.LOG_DEBUG, "rapidfire: Failed RF upload. Reason: %s" % (e,)) else: # No exception thrown, but we're still not done. # We have to also check for a bad station ID or password. # It will have the error encoded in the return message: for line in _response: # PWSweather signals with 'ERROR', WU with 'INVALID': if line.startswith('ERROR') or line.startswith('INVALID'): # Bad login. No reason to retry. Log it and raise an exception. syslog.syslog(syslog.LOG_ERR, "rapidfire: WU returns %s. Aborting." % (line,)) raise weewx.restful.FailedPost, line # Does not seem to be an error. We're done. return