On Tuesday, 2 April 2019 06:31:54 UTC-3, Hartmut Schweidler wrote: > > I use your files weatherCloud and Weather365 for Python3 > ? module are not loaded / callable >
Hartmut, I fixed the errors. Attached new versions of wcloud.py and weather365.py. Luc
# $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
