Tom, Improved versions for meteotemplate.py, wcloud.py and weather365.py. The should run on both Python 2 and Python 3.
Luc
#!/usr/bin/env python # Copyright 2016-2017 Matthew Wall # Licensed under the terms of the GPLv3 """ Meteotemplate is a weather website system written in PHP by Jachym. http://meteotemplate.com This is a weewx extension that uploads data to a Meteotemplate server. It uses the API described in the meteotemplate wiki: http://www.meteotemplate.com/web/wiki/wikiAPI.php The set of fields actually sent depends on the sensors available and the way the hardware sends data from those fields. More specifically, this extension works with the following API specification: URL: http[s]://TEMPLATE_ROOT/api.php Parameters: PASS - the "update password" in meteotemplate settings U - datetime as epoch T - temperature (C) H - humidity (%) P - barometer (mbar) W - wind speed (km/h) G - wind gust (km/h) B - wind direction (0-359) R - daily cumulative rain (mm since midnight) RR - current rain rate (mm/h) S - solar radiation (W/m^2) UV - ultraviolet index TIN - indoor temperature (C) HIN - indoor humidity (%) ... A parameter is ignored if: - it is not provided in the URL - it is blank (e.g., T=&H=&P=) - is set to null (e.g., T=null&H=null) Each request must contain PASS, U, and at least one parameter. Data can be sent at any interval. If the interval is shorter than 5 minutes, data will be cached then aggregated. The meteotemplate database is updated every 5 minutes. Battery status is handled properly for battery status fields in the default schema. Battery voltages are not included (the meteotemplate API has no provision for battery voltage). """ from __future__ import print_function # Python 2/3 compatiblity from distutils.version import StrictVersion import sys import syslog import time # Python 2/3 compatiblity try: import Queue as queue # python 2 from urllib import urlencode # python 2 from urllib2 import Request # python 2 except ImportError: import queue # python 3 from urllib.parse import urlencode # python 3 from urllib.request import Request # python 3 import weewx import weewx.restx import weewx.units from weeutil.weeutil import to_bool, accumulateLeaves, startOfDay, list_as_string VERSION = "0.10" REQUIRED_WEEWX = "3.5.0" if StrictVersion(weewx.__version__) < StrictVersion(REQUIRED_WEEWX): raise weewx.UnsupportedFeature("weewx %s or greater is required, found %s" % (REQUIRED_WEEWX, weewx.__version__)) def logmsg(level, msg): syslog.syslog(level, 'restx: Meteotemplate: %s' % msg) def logdbg(msg): logmsg(syslog.LOG_DEBUG, msg) def loginf(msg): logmsg(syslog.LOG_INFO, msg) def logerr(msg): logmsg(syslog.LOG_ERR, msg) class Meteotemplate(weewx.restx.StdRESTbase): DEFAULT_URL = 'http://localhost/template/api.php' def __init__(self, engine, cfg_dict): """This service recognizes standard restful options plus the following: Parameters: password: the shared key for uploading data server_url: full URL to the meteotemplate ingest script """ super(Meteotemplate, self).__init__(engine, cfg_dict) loginf("service version is %s" % VERSION) try: site_dict = cfg_dict['StdRESTful']['Meteotemplate'] site_dict = accumulateLeaves(site_dict, max_level=1) site_dict['password'] except KeyError as e: logerr("Data will not be uploaded: Missing option %s" % e) return site_dict.get('server_url', Meteotemplate.DEFAULT_URL) binding = list_as_string(site_dict.pop('binding', 'archive').lower()) try: _mgr_dict = weewx.manager.get_manager_dict_from_config( cfg_dict, 'wx_binding') site_dict['manager_dict'] = _mgr_dict except weewx.UnknownBinding: pass self._queue = queue.Queue() try: self._thread = MeteotemplateThread(self._queue, **site_dict) except weewx.ViolatedPrecondition as e: loginf("Data will not be posted: %s" % e) return self._thread.start() if 'loop' in binding: self.bind(weewx.NEW_LOOP_PACKET, self.handle_new_loop) if 'archive' in binding: self.bind(weewx.NEW_ARCHIVE_RECORD, self.handle_new_archive) if 'both' in binding: self.bind(weewx.NEW_LOOP_PACKET, self.handle_new_loop) self.bind(weewx.NEW_ARCHIVE_RECORD, self.handle_new_archive) loginf("Data will be uploaded to %s" % site_dict['server_url']) def handle_new_loop(self, event): self._queue.put(event.packet) def handle_new_archive(self, event): self._queue.put(event.record) class MeteotemplateThread(weewx.restx.RESTThread): try: max_integer = sys.maxint # python 2 except AttributeError: max_integer = sys.maxsize # python 3 def __init__(self, queue, password, server_url, skip_upload=False, manager_dict=None, post_interval=None, max_backlog=max_integer, stale=None, log_success=True, log_failure=True, timeout=60, max_tries=3, retry_wait=5): super(MeteotemplateThread, self).__init__( queue, protocol_name='Meteotemplate', manager_dict=manager_dict, post_interval=post_interval, max_backlog=max_backlog, stale=stale, log_success=log_success, log_failure=log_failure, max_tries=max_tries, timeout=timeout, retry_wait=retry_wait) self.server_url = server_url self.password = password self.skip_upload = to_bool(skip_upload) self.field_map = self.create_default_field_map() # FIXME: make field map changes available via config file def process_record(self, record, dbm): if dbm: record = self.get_record(record, dbm) url = self.get_url(record) if weewx.debug >= 2: logdbg('url: %s' % url) if self.skip_upload: raise weewx.restx.AbortedPost() req = Request(url) req.add_header("User-Agent", "weewx/%s" % weewx.__version__) self.post_with_retries(req) def check_response(self, response): txt = response.read().decode('utf-8') if txt != 'Success': raise weewx.restx.FailedPost("Server returned '%s'" % txt) if self.log_success: logdbg("upload complete: %s" % txt) def get_url(self, record): record = weewx.units.to_std_system(record, weewx.METRIC) if 'dayRain' in record and record['dayRain'] is not None: record['dayRain'] *= 10.0 # convert to mm if 'rainRate' in record and record['rainRate'] is not None: record['rainRate'] *= 10.0 # convert to mm/h parts = dict() parts['PASS'] = self.password parts['U'] = record['dateTime'] parts['SW'] = "weewx-%s" % weewx.__version__ for k in self.field_map: if (self.field_map[k][0] in record and record[self.field_map[k][0]] is not None): parts[k] = self._fmt(record.get(self.field_map[k][0]), self.field_map[k][1]) return "%s?%s" % (self.server_url, urlencode(parts)) @staticmethod def _fmt(x, places=3): fmt = "%%.%df" % places try: return fmt % x except TypeError: pass return x @staticmethod def create_default_field_map(): fm = { 'T': ('outTemp', 2), # degree_C 'H': ('outHumidity', 1), # percent 'P': ('barometer', 3), # mbar 'UGP': ('pressure', 3), # mbar 'W': ('windSpeed', 2), # km/h 'G': ('windGust', 2), # km/h 'B': ('windDir', 0), # degree_compass 'RR': ('rainRate', 3), # mm/h 'R': ('dayRain', 3), # mm 'S': ('radiation', 3), # W/m^2 'UV': ('UV', 0), 'TIN': ('inTemp', 2), # degree_C 'HIN': ('inHumidity', 1), # percent 'SN': ('daySnow', 3), # mm 'SD': ('snowDepth', 3), # mm 'L': ('lightning', 0), 'NL': ('noise', 2)} # dB for i in range(1, 9): fm['T%d' % i] = ('extraTemp%d' % i, 2) # degree_C fm['H%d' % i] = ('extraHumid%d' % i, 1) # percent fm['TS%d' % i] = ('soilTemp%d' % i, 2) # degree_C fm['TSD%d' % i] = ('soilTempDepth%d' % i, 2) # cm fm['LW%d' % i] = ('leafWet%d' % i, 1) fm['LT%d' % i] = ('leafTemp%d' % i, 2) # degree_C fm['SM%d' % i] = ('soilMoist%d' % i, 1) fm['CO2_%d' % i] = ('co2_%d' % i, 3) # ppm fm['NO2_%d' % i] = ('no2_%d' % i, 3) # ppm fm['CO_%d' % i] = ('co_%d' % i, 3) # ppm fm['SO2_%d' % i] = ('so2_%d' % i, 3) # ppb fm['O3_%d' % i] = ('o3_%d' % i, 3) # ppb fm['pp%d' % i] = ('pp%d' % i, 3) # ug/m^3 fm['TXBAT'] = ('txBatteryStatus', 0) fm['WBAT'] = ('windBatteryStatus', 0) fm['RBAT'] = ('rainBatteryStatus', 0) fm['TBAT'] = ('outTempBatteryStatus', 0) fm['TINBAT'] = ('inTempBatteryStatus', 0) return fm # Do direct testing of this extension like this: # PYTHONPATH=WEEWX_BINDIR python WEEWX_BINDIR/user/meteotemplate.py if __name__ == "__main__": import optparse usage = """%prog [--url URL] [--pw password] [--version] [--help]""" syslog.openlog('meteotemplate', syslog.LOG_PID | syslog.LOG_CONS) syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_DEBUG)) parser = optparse.OptionParser(usage=usage) parser.add_option('--version', dest='version', action='store_true', help='display driver version') parser.add_option('--url', dest='url', default=Meteotemplate.DEFAULT_URL, help='full URL to the server script') parser.add_option('--pw', dest='pw', help='upload password') (options, args) = parser.parse_args() if options.version: print("meteotemplate uploader version %s" % VERSION) exit(0) print("uploading to %s" % options.url) weewx.debug = 2 queue = queue.Queue() t = MeteotemplateThread( queue, manager_dict=None, password=options.pw, server_url=options.url) t.process_record({'dateTime': int(time.time() + 0.5), 'usUnits': weewx.US, 'outTemp': 32.5, 'inTemp': 75.8, 'outHumidity': 24}, None)
# $Id: wcloud.py 1391 2015-12-28 14:51:29Z mwall $ # Copyright 2014 Matthew Wall """ This is a weewx extension that uploads data to WeatherCloud. http://weather.weathercloud.com Based on weathercloud API documentation v0.5 as of 15oct2014. The preferred upload frequency (post_interval) is one record every 10 minutes. These are the possible states for sensor values: - sensor exists and returns valid value - sensor exists and returns invalid value that passes StdQC - sensor exists and returns None (e.g., windDir when windSpeed is zero) - sensor exists but is not working - sensor does not exist Regarding None/NULL values, the folks at weathercloud say the following: "In order to fix this issue, WeeWX should send our error code (-32768) instead of omitting that variable in the frame. This way we know that the device is able to measure the variable but that it's not currently working." Minimal Configuration: [StdRESTful] [[WeatherCloud]] id = WEATHERCLOUD_ID key = WEATHERCLOUD_KEY """ import re import sys import syslog import time # Python 2/3 compatiblity try: import Queue as queue # python 2 from urllib import urlencode # python 2 from urllib2 import Request # python 2 except ImportError: import queue # python 3 from urllib.parse import urlencode # python 3 from urllib.request import Request # python 3 import weewx import weewx.restx import weewx.units import weewx.wxformulas from weeutil.weeutil import to_bool, accumulateLeaves VERSION = "0.12" if weewx.__version__ < "3": raise weewx.UnsupportedFeature("weewx 3 is required, found %s" % weewx.__version__) def logmsg(level, msg): syslog.syslog(level, 'restx: WeatherCloud: %s' % msg) def logdbg(msg): logmsg(syslog.LOG_DEBUG, msg) def loginf(msg): logmsg(syslog.LOG_INFO, msg) def logerr(msg): logmsg(syslog.LOG_ERR, msg) # weewx uses a status of 1 to indicate failure, wcloud uses 0 def _invert(x): if x is None: return None if x == 0: return 1 return 0 # utility to convert to METRICWX windspeed def _convert_windspeed(v, from_unit_system): if from_unit_system is None: return None if from_unit_system != weewx.METRICWX: (from_unit, _) = weewx.units.getStandardUnitType( from_unit_system, 'windSpeed') from_t = (v, from_unit, 'group_speed') v = weewx.units.convert(from_t, 'meter_per_second')[0] return v # FIXME: this formula is suspect def _calc_thw(heatindex_C, windspeed_mps): if heatindex_C is None or windspeed_mps is None: return None windspeed_mph = 2.25 * windspeed_mps heatindex_F = 32 + heatindex_C * 9 / 5 thw_F = heatindex_F - (1.072 * windspeed_mph) thw_C = (thw_F - 32) * 5 / 9 return thw_C def _get_windavg(dbm, ts, interval=600): sts = ts - interval val = dbm.getSql("SELECT AVG(windSpeed) FROM %s " "WHERE dateTime>? AND dateTime<=?" % dbm.table_name, (sts, ts)) return val[0] if val is not None else None # weathercloud wants "10-min maximum gust of wind". some hardware reports # a wind gust, others do not, so try to deal with both. def _get_windhi(dbm, ts, interval=600): sts = ts - interval val = dbm.getSql("""SELECT MAX(CASE WHEN windSpeed >= windGust THEN windSpeed ELSE windGust END) FROM %s WHERE dateTime>? AND dateTime<=?""" % dbm.table_name, (sts, ts)) return val[0] if val is not None else None def _get_winddiravg(dbm, ts, interval=600): sts = ts - interval val = dbm.getSql("SELECT AVG(windDir) FROM %s " "WHERE dateTime>? AND dateTime<=?" % dbm.table_name, (sts, ts)) return val[0] if val is not None else None class WeatherCloud(weewx.restx.StdRESTbase): def __init__(self, engine, config_dict): """This service recognizes standard restful options plus the following: id: WeatherCloud identifier key: WeatherCloud key """ super(WeatherCloud, self).__init__(engine, config_dict) loginf("service version is %s" % VERSION) try: site_dict = config_dict['StdRESTful']['WeatherCloud'] site_dict = accumulateLeaves(site_dict, max_level=1) site_dict['id'] site_dict['key'] except KeyError as e: logerr("Data will not be posted: Missing option %s" % e) return site_dict['manager_dict'] = weewx.manager.get_manager_dict( config_dict['DataBindings'], config_dict['Databases'], 'wx_binding') self.archive_queue = queue.Queue() self.archive_thread = WeatherCloudThread(self.archive_queue, **site_dict) self.archive_thread.start() self.bind(weewx.NEW_ARCHIVE_RECORD, self.new_archive_record) loginf("Data will be uploaded for id=%s" % site_dict['id']) def new_archive_record(self, event): self.archive_queue.put(event.record) class WeatherCloudThread(weewx.restx.RESTThread): _SERVER_URL = 'http://api.weathercloud.net/v01/set' # this data map supports the default database schema # FIXME: design a config option to override this map # wcloud_name weewx_name format multiplier _DATA_MAP = {'temp': ('outTemp', '%.0f', 10.0), # C * 10 'hum': ('outHumidity', '%.0f', 1.0), # percent 'wdir': ('windDir', '%.0f', 1.0), # degree 'wspd': ('windSpeed', '%.0f', 10.0), # m/s * 10 'bar': ('barometer', '%.0f', 10.0), # hPa * 10 'rain': ('dayRain', '%.0f', 10.0), # mm * 10 'rainrate': ('rainRate', '%.0f', 10.0), # mm/hr * 10 'tempin': ('inTemp', '%.0f', 10.0), # C * 10 'humin': ('inHumidity', '%.0f', 1.0), # percent 'uvi': ('UV', '%.0f', 10.0), # index * 10 'solarrad': ('radiation', '%.0f', 10.0), # W/m^2 * 10 'et': ('EV', '%.0f', 10.0), # mm * 10 'chill': ('windchill', '%.0f', 10.0), # C * 10 'heat': ('heatindex', '%.0f', 10.0), # C * 10 'dew': ('dewpoint', '%.0f', 10.0), # C * 10 'battery': ('consBatteryVoltage', '%.0f', 100.0), # V * 100 'temp01': ('extraTemp1', '%.0f', 10.0), # C * 10 'temp02': ('extraTemp2', '%.0f', 10.0), # C * 10 'temp03': ('extraTemp3', '%.0f', 10.0), # C * 10 'temp04': ('leafTemp1', '%.0f', 10.0), # C * 10 'temp05': ('leafTemp2', '%.0f', 10.0), # C * 10 'temp06': ('soilTemp1', '%.0f', 10.0), # C * 10 'temp07': ('soilTemp2', '%.0f', 10.0), # C * 10 'temp08': ('soilTemp3', '%.0f', 10.0), # C * 10 'temp09': ('soilTemp4', '%.0f', 10.0), # C * 10 'temp10': ('heatingTemp4', '%.0f', 10.0), # C * 10 'leafwet01': ('leafWet1', '%.0f', 1.0), # [0,15] 'leafwet02': ('leafWet2', '%.0f', 1.0), # [0,15] 'hum01': ('extraHumid1', '%.0f', 1.0), # percent 'hum02': ('extraHumid2', '%.0f', 1.0), # percent 'soilmoist01': ('soilMoist1', '%.0f', 1.0), # Cb [0,200] 'soilmoist02': ('soilMoist2', '%.0f', 1.0), # Cb [0,200] 'soilmoist03': ('soilMoist3', '%.0f', 1.0), # Cb [0,200] 'soilmoist04': ('soilMoist4', '%.0f', 1.0), # Cb [0,200] # these are calculated by this extension # 'thw': ('thw', '%.0f', 10.0), # C * 10 'wspdhi': ('windhi', '%.0f', 10.0), # m/s * 10 'wspdavg': ('windavg', '%.0f', 10.0), # m/s * 10 'wdiravg': ('winddiravg', '%.0f', 1.0), # degree 'heatin': ('inheatindex', '%.0f', 10.0), # C * 10 'dewin': ('indewpoint', '%.0f', 10.0), # C * 10 'battery01': ('bat01', '%.0f', 1.0), # 0 or 1 'battery02': ('bat02', '%.0f', 1.0), # 0 or 1 'battery03': ('bat03', '%.0f', 1.0), # 0 or 1 'battery04': ('bat04', '%.0f', 1.0), # 0 or 1 'battery05': ('bat05', '%.0f', 1.0), # 0 or 1 # these are in the wcloud api but are not yet implemented # 'tempagroXX': ('??', '%.0f', 10.0), # C * 10 # 'wspdXX': ('??', '%.0f', 10.0), # m/s * 10 # 'wspdavgXX': ('??', '%.0f', 10.0), # m/s * 10 # 'wspdhiXX': ('??', '%.0f', 10.0), # m/s * 10 # 'wdirXX': ('??', '%.0f', 1.0), # degree # 'wdiravgXX': ('??', '%.0f', 1.0), # degree # 'bartrend': ('??', '%.0f', 1.0), # -60,-20,0,20,60 # 'forecast': ('??', '%.0f', 1.0), # 'forecasticon': ('??', '%.0f', 1.0), } try: max_integer = sys.maxint # python 2 except AttributeError: max_integer = sys.maxsize # python 3 def __init__(self, queue, id, key, manager_dict, server_url=_SERVER_URL, skip_upload=False, post_interval=600, max_backlog=max_integer, stale=None, log_success=True, log_failure=True, timeout=60, max_tries=3, retry_wait=5): super(WeatherCloudThread, self).__init__(queue, protocol_name='WeatherCloud', manager_dict=manager_dict, post_interval=post_interval, max_backlog=max_backlog, stale=stale, log_success=log_success, log_failure=log_failure, max_tries=max_tries, timeout=timeout, retry_wait=retry_wait) self.id = id self.key = key self.server_url = server_url self.skip_upload = to_bool(skip_upload) def process_record(self, record, dbm): r = self.get_record(record, dbm) url = self.get_url(r) if self.skip_upload: loginf("skipping upload") return req = Request(url) req.add_header("User-Agent", "weewx/%s" % weewx.__version__) self.post_with_retries(req) # calculate derived quantities and other values needed by wcloud def get_record(self, record, dbm): rec = super(WeatherCloudThread, self).get_record(record, dbm) # put everything into units required by weathercloud rec = weewx.units.to_METRICWX(rec) # calculate additional quantities rec['windavg'] = _get_windavg(dbm, record['dateTime']) rec['windhi'] = _get_windhi(dbm, record['dateTime']) rec['winddiravg'] = _get_winddiravg(dbm, record['dateTime']) # ensure wind direction is in [0,359] if rec['windDir'] is not None: if int(rec['windDir']) > 359: rec['windDir'] -= 360 if rec['winddiravg'] is not None: if int(rec['winddiravg']) > 359: rec['winddiravg'] -= 360 # these observations are non-standard, so do unit conversions directly rec['windavg'] = _convert_windspeed(rec['windavg'], record['usUnits']) rec['windhi'] = _convert_windspeed(rec['windhi'], record['usUnits']) if 'inTemp' in rec and 'inHumidity' in rec: rec['inheatindex'] = weewx.wxformulas.heatindexC( rec['inTemp'], rec['inHumidity']) rec['indewpoint'] = weewx.wxformulas.dewpointC( rec['inTemp'], rec['inHumidity']) # if 'heatindex' in rec and 'windSpeed' in rec: # rec['thw'] = _calc_thw(rec['heatindex'], rec['windSpeed']) if 'txBatteryStatus' in record: rec['bat01'] = _invert(record['txBatteryStatus']) if 'windBatteryStatus' in record: rec['bat02'] = _invert(record['windBatteryStatus']) if 'rainBatteryStatus' in record: rec['bat03'] = _invert(record['rainBatteryStatus']) if 'outTempBatteryStatus' in record: rec['bat04'] = _invert(record['outTempBatteryStatus']) if 'inTempBatteryStatus' in record: rec['bat05'] = _invert(record['inTempBatteryStatus']) return rec def get_url(self, record): # put data into expected structure and format values = {} values['ver'] = str(weewx.__version__) values['type'] = 251 # identifier assigned to weewx by weathercloud values['wid'] = self.id values['key'] = self.key time_tt = time.gmtime(record['dateTime']) values['time'] = time.strftime("%H%M", time_tt) # assumes leading zeros values['date'] = time.strftime("%Y%m%d", time_tt) for key in self._DATA_MAP: rkey = self._DATA_MAP[key][0] if rkey in record and record[rkey] is not None: v = record[rkey] * self._DATA_MAP[key][2] values[key] = self._DATA_MAP[key][1] % v url = self.server_url + '?' + urlencode(values) if weewx.debug >= 2: logdbg('url: %s' % re.sub(r"key=[^\&]*", "key=XXX", url)) return url
# $Id: weather365.py 0957 14-04-2017 1.3.1$ # Copyright 2017 Frank Bandle # based on Scripts written by M.Wall # Thanks Luc Heijst for testing and correction """ Upload data to weather365.net https://channel1.weather365.net/stations/ [StdRESTful] [[Weather365]] stationid = INSERT_STATIONID_HERE password = INSERT_PASSWORD_HERE """ import re import sys import syslog import time # Python 2/3 compatiblity try: import Queue as queue # python 2 from urllib import urlencode # python 2 from urllib2 import Request # python 2 except ImportError: import queue # python 3 from urllib.parse import urlencode # python 3 from urllib.request import Request # python 3 import weewx import weewx.restx import weewx.units from weeutil.weeutil import to_bool, accumulateLeaves VERSION = "1.4.2" if weewx.__version__ < "3": raise weewx.UnsupportedFeature("weewx 3 is required, found %s" % weewx.__version__) def logmsg(level, msg): syslog.syslog(level, 'restx: Weather365: %s' % msg) def logdbg(msg): logmsg(syslog.LOG_DEBUG, msg) def loginf(msg): logmsg(syslog.LOG_INFO, msg) def logerr(msg): logmsg(syslog.LOG_ERR, msg) class Weather365(weewx.restx.StdRESTbase): def __init__(self, engine, config_dict): """This service recognizes standard restful options plus the following: stationid = INSERT_STATIONID_HERE password = INSERT_PASSWORD_HERE latitude: Station latitude in decimal degrees Default is station latitude longitude: Station longitude in decimal degrees Default is station longitude """ super(Weather365, self).__init__(engine, config_dict) loginf("service version is %s" % VERSION) try: site_dict = config_dict['StdRESTful']['Weather365'] site_dict = accumulateLeaves(site_dict, max_level=1) site_dict['stationid'] site_dict['password'] except KeyError as e: logerr("Data will not be posted: Missing option %s" % e) return site_dict.setdefault('latitude', engine.stn_info.latitude_f) site_dict.setdefault('longitude', engine.stn_info.longitude_f) site_dict.setdefault('altitude', engine.stn_info.altitude_vt[0]) site_dict['manager_dict'] = weewx.manager.get_manager_dict( config_dict['DataBindings'], config_dict['Databases'], 'wx_binding') self.archive_queue = queue.Queue() self.archive_thread = Weather365Thread(self.archive_queue, **site_dict) self.archive_thread.start() self.bind(weewx.NEW_ARCHIVE_RECORD, self.new_archive_record) loginf("Data will be uploaded for station id %s" % site_dict['stationid']) def new_archive_record(self, event): self.archive_queue.put(event.record) class Weather365Thread(weewx.restx.RESTThread): _SERVER_URL = 'https://channel1.weather365.net/stations/index.php' _DATA_MAP = {'winddir': ('windDir', '%.0f', 1.0), # degrees 'windspeed': ('windSpeed', '%.1f', 0.2777777777), # m/s 'windgust': ('windGust', '%.1f', 0.2777777777), # m/s 't2m': ('outTemp', '%.1f', 1.0), # C 'relhum': ('outHumidity', '%.0f', 1.0), # percent 'press': ('barometer', '%.3f', 1.0), # hpa? 'rainh': ('hourRain', '%.2f', 10.0), # mm 'raind': ('dayRain', '%.2f', 10.0), # mm 'rainrate': ('rainRate', '%.2f', 1.0), # mm/hr 'uvi': ('UV', '%.1f', 1.0), # index * 1 'radi': ('radiation', '%.1f', 1.0), # W/m^2 * 1 'et': ('ET', '%.5f', 10.0), # mm * 1 'wchill': ('windchill', '%.1f', 1.0), # C * 1 'heat': ('heatindex', '%.1f', 1.0), # C * 1 'dew2m': ('dewpoint', '%.1f', 1.0), # C * 1 'rxsignal': ('rxCheckPercent', '%.0f', 1.0), # percent / dB 'cloudbase': ('cloudbase', '%.1f', 1.0), # m 'windrun': ('windrun', '%.1f', 1.0), # m 'humidex': ('humidex', '%.1f', 1.0), # 'appTemp': ('appTemp', '%.1f', 1.0), # C 'soiltemp': ('soilTemp1', '%.1f', 1.0), # C 'soiltemp2': ('soilTemp2', '%.1f', 1.0), # C 'soiltemp3': ('soilTemp3', '%.1f', 1.0), # C 'soiltemp4': ('soilTemp4', '%.1f', 1.0), # C 'soilmoisture': ('soilMoist1', '%.1f', 1.0), # % 'soilmoisture2': ('soilMoist2', '%.1f', 1.0), # % 'soilmoisture3': ('soilMoist3', '%.1f', 1.0), # % 'soilmoisture4': ('soilMoist4', '%.1f', 1.0), # % 'leafwetness': ('leafWet1', '%.1f', 1.0), # % 'leafwetness2': ('leafWet2', '%.1f', 1.0), # % 'temp2': ('extraTemp1', '%.1f', 1.0), # C 'temp3': ('extraTemp2', '%.1f', 1.0), # C 'temp4': ('extraTemp3', '%.1f', 1.0), # C 'humidity2': ('extraHumid1', '%.0f', 1.0), # % 'humidity3': ('extraHumid2', '%.0f', 1.0), # % 'txbattery': ('txBatteryStatus', '%.0f', 1.0), # % } try: max_integer = sys.maxint # python 2 except AttributeError: max_integer = sys.maxsize # python 3 def __init__(self, queue, stationid, password, latitude, longitude, altitude, manager_dict, server_url=_SERVER_URL, skip_upload=False, post_interval=None, max_backlog=max_integer, stale=None, log_success=True, log_failure=True, timeout=60, max_tries=3, retry_wait=5): super(Weather365Thread, self).__init__(queue, protocol_name='Weather365', manager_dict=manager_dict, post_interval=post_interval, max_backlog=max_backlog, stale=stale, log_success=log_success, log_failure=log_failure, max_tries=max_tries, timeout=timeout, retry_wait=retry_wait) self.latitude = float(latitude) self.longitude = float(longitude) self.altitude = float(altitude) self.stationid = stationid self.server_url = server_url self.skip_upload = to_bool(skip_upload) def process_record(self, record, dbm): r = self.get_record(record, dbm) data = self.get_data(r) url_data = urlencode(data).encode('utf-8') if self.skip_upload: loginf("skipping upload") return req = Request(self.server_url, url_data) loginf("Data uploaded to %s is: (%s)" % (self.server_url, url_data)) req.get_method = lambda: 'POST' req.add_header("User-Agent", "weewx/%s" % weewx.__version__) self.post_with_retries(req) def check_response(self, response): txt = response.read() if txt.find('invalid-login-data') >= 0: raise weewx.restx.BadLogin(txt) elif not txt.startswith('INSERT'): raise weewx.restx.FailedPost("Server returned '%s'" % txt) def get_data(self, in_record): # put everything into the right units record = weewx.units.to_METRIC(in_record) # put data into expected scaling, structure, and format values = {} values['lat'] = str(self.latitude) values['long'] = str(self.longitude) values['alt'] = str(self.altitude) # meter values['stationid'] = self.stationid values['prec_time'] = 60 values['datum'] = time.strftime('%Y%m%d%H%M', time.localtime(record['dateTime'])) values['utcstamp'] = int(record['dateTime']) for key in self._DATA_MAP: rkey = self._DATA_MAP[key][0] if rkey in record and record[rkey] is not None: v = record[rkey] * self._DATA_MAP[key][2] values[key] = self._DATA_MAP[key][1] % v return values
