# $Id: awekas.py 702 2013-10-29 13:17:26Z mwall $
# Copyright 2013 Matthew Wall

#==============================================================================
# AWEKAS
#==============================================================================
# Upload data to AWEKAS - Automatisches WEtterKArten System
#
# To enable this module, add the following to weewx.conf:
#
# [StdRESTful]
#     ...
#     [[AWEKAS]]
#         username = AWEKAS_USERNAME
#         password = AWEKAS_PASSWORD
#         driver = user.awekas.AWEKAS

import hashlib
import httplib
import socket
import syslog
import time
import urllib2

from weewx.restful import REST, SkippedPost, FailedPost
import weewx.units
import weeutil.weeutil

AWEKAS_DRIVER_VERSION = 0.3
AWEKAS_SERVER_URL = 'http://data.awekas.at/eingabe_pruefung.php'

def logmsg(level, msg):
    syslog.syslog(level, 'awekas: %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 AWEKAS(REST):
    """Upload using the AWEKAS protocol.

    The AWEKAS server expects a single string of values delimited by
    semicolons.  The position of each value matters, for example position 1
    is the awekas username and position 2 is the awekas password.

    Positions 1-25 are defined for the basic API:

    Pos1: user (awekas username)
    Pos2: password (awekas password MD5 Hash)
    Pos3: date (dd.mm.yyyy) (varchar)
    Pos4: time (hh:mm) (varchar)
    Pos5: temperature (C) (float)
    Pos6: humidity (%) (int)
    Pos7: air pressure (hPa) (float)
    Pos8: precipitation (rain at this day) (float)
    Pos9: wind speed (km/h) float)
    Pos10: wind direction (degree) (int)
    Pos11: weather condition (int)
            0=clear warning
            1=clear
            2=sunny sky
            3=partly cloudy
            4=cloudy
            5=heavy cloundy
            6=overcast sky
            7=fog
            8=rain showers
            9=heavy rain showers
           10=light rain
           11=rain
           12=heavy rain
           13=light snow
           14=snow
           15=light snow showers
           16=snow showers
           17=sleet
           18=hail
           19=thunderstorm
           20=storm
           21=freezing rain
           22=warning
           23=drizzle
           24=heavy snow
           25=heavy snow showers
    Pos12: warning text (varchar)
    Pos13: snow high (cm) (int) if no snow leave blank
    Pos14: language (varchar)
           de=german; en=english; it=italian; fr=french; nl=dutch
    Pos15: tendency (int)
           -2 = high falling
           -1 = falling
            0 = steady
            1 = rising
            2 = high rising
    Pos16. wind gust (km/h) (float)
    Pos17: solar radiation (W/m^2) (float) 
    Pos18: UV Index (float)
    Pos19: brightness (LUX) (int)
    Pos20: sunshine hours today (float)
    Pos21: soil temperature (degree C) (float)
    Pos22: rain rate (mm/h) (float)
    Pos23: software flag NNNN_X.Y, for example, WLIP_2.15
    Pos24: longitude (float)
    Pos25: latitude (float)

    positions 26-111 are defined for API2
        """

    _formats = {'barometer'   : '%.3f',
                'outTemp'     : '%.1f',
                'outHumidity' : '%.0f',
                'windSpeed'   : '%.1f',
                'windDir'     : '%.0f',
                'windGust'    : '%.1f',
                'dewpoint'    : '%.1f',
                'hourRain'    : '%.2f',
                'dayRain'     : '%.2f',
                'radiation'   : '%.2f',
                'UV'          : '%.2f',
                'rainRate'    : '%.2f'}

    def __init__(self, site, **kwargs):
        """Initialize for posting data to AWEKAS.

        site: The upload site ("AWEKAS")

        username: AWEKAS user name
        [Required]

        password: AWEKAS password
        [Required]

        language: Possible values include de, en, it, fr, nl
        [Required.  Default is de]

        interval: The interval in seconds between posts.
        AWEKAS requests that uploads happen no more often than 5 minutes, so
        this should be set to no less than 300.
        [Optional.  Default is 300]

        max_tries: Maximum number of tries before giving up
        [Optional.  Default is 3]

        latitude: Station latitude
        [Required]

        longitude: Station longitude
        [Required]
        
        server_url: Base URL for the AWEKAS server
        [Required.  Default is http://data.awekas.at/eingabe_pruefung.php]
        """
        self.site = site
        self.server_url = kwargs.get('server_url', AWEKAS_SERVER_URL)
        self.latitude = float(kwargs['latitude'])
        self.longitude = float(kwargs['longitude'])
        self.username = kwargs['username']
        self.password = kwargs['password']
        self.language = kwargs.get('language', 'de')
        self.interval = int(kwargs.get('interval', 300))
        self.max_tries = int(kwargs.get('max_tries', 3))
        self._lastpost = None
        # FIXME: restful needs this, but awekas does not
        self.station = kwargs.get('station', None)

    # FIXME: specify desired fields to base class so we do not have to do
    # a second lookup
    def extractRecordFrom(self, archive, time_ts):
        """The base class does not include rainRate, so we have to get it."""
        record = REST.extractRecordFrom(self, archive, time_ts)
        sqlrec = archive.getSql('select rainRate from archive where dateTime=?', (time_ts,))
        datadict = dict(zip(['rainRate'], sqlrec))
        if datadict.has_key('rainRate'):
            record['rainRate'] = datadict['rainRate']
        return record

    def postData(self, archive, time_ts):
        """Post data to the AWEKAS server.

        archive: An instance of weewx.archive.Archive
        
        time_ts: The record desired as a unix epoch time."""
        
        last_ts = archive.lastGoodStamp()

        if time_ts != last_ts:
            msg = "%s: Record %s is not last record" % \
                (self.site, weeutil.weeutil.timestamp_to_string(time_ts))
            logdbg(msg)
            raise SkippedPost, msg
        
        if self._lastpost and time_ts - self._lastpost < self.interval:
            msg = "%s: Wait interval (%d) has not passed." % \
                (self.site, self.interval)
            raise SkippedPost, msg

        url = self.getURL(archive, time_ts)
        logdbg('url is %s' % url)

        for count in range(self.max_tries):
            try:
                response = urllib2.urlopen(url)
            except (urllib2.URLError, socket.error, httplib.BadStatusLine), e:
                logerr("Failed upload attempt %d to %s" % (count+1, self.site))
                logerr("   ****  Reason: %s" % (e,))
                logdbg("   ****  URL: %s" % (url))
            else:
                for line in response:
                    if not line.startswith('OK'):
                        msg = "%s returned '%s'" % (self.site, line)
                        logerr(msg)
                        raise FailedPost, msg
                self._lastpost = time_ts
                return
        else:
            msg = "Failed upload to %s after %d tries" % (
                self.site, self.max_tries)
            logerr(msg)
            raise IOError, msg

    def getURL(self, archive, time_ts):
        """Return an URL for posting to AWEKAS.
        
        archive: An instance of weewx.archive.Archive
        
        time_ts: The record desired as a unix epoch time.
        """
        record = self.extractRecordFrom(archive, time_ts)

        # put everything into the units AWEKAS wants
        if record['usUnits'] != weewx.METRIC:
            converter = weewx.units.StdUnitConverters[weewx.METRIC]
            record = converter.convertDict(record)
        if record.has_key('dayRain') and record['dayRain'] is not None:
            record['dayRain'] = record['dayRain'] * 10
        if record.has_key('rainRate') and record['rainRate'] is not None:
            record['rainRate'] = record['rainRate'] * 10

        # assemble an array of values in the order AWEKAS wants
        values = [self.username]
        m = hashlib.md5()
        m.update(self.password)
        values.append(m.hexdigest())
        time_tt = time.gmtime(record['dateTime'])
        values.append(time.strftime("%d.%m.%Y", time_tt))
        values.append(time.strftime("%H:%M", time_tt))
        values.append(self._format(record, 'outTemp')) # C
        values.append(self._format(record, 'outHumidity')) # %
        values.append(self._format(record, 'barometer')) # mbar
        values.append(self._format(record, 'dayRain')) # mm?
        values.append(self._format(record, 'windSpeed')) # km/h
        values.append(self._format(record, 'windDir'))
        values.append('') # weather condition
        values.append('') # warning text
        values.append('') # snow high
        values.append(self.language)
        values.append('') # tendency
        values.append(self._format(record, 'windGust')) # km/h
        values.append(self._format(record, 'radiation')) # W/m^2
        values.append(self._format(record, 'UV')) # uv index
        values.append('') # brightness in lux
        values.append('') # sunshine hours
        values.append('') # soil temperature
        values.append(self._format(record, 'rainRate')) # mm/h
        values.append('weewx_%s' % weewx.__version__)
        values.append(str(self.longitude))
        values.append(str(self.latitude))

        valstr = ';'.join(values)
        return self.server_url + '?val=' + valstr

    def _format(self, record, label):
        if record.has_key(label) and record[label] is not None:
            if AWEKAS._formats.has_key(label):
                return AWEKAS._formats[label] % record[label]
            return str(record[label])
        return ''
