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

Reply via email to