Hallo Gary,

 1. I have created a copy of weewxacer database

2. the function record_generator = *WdGenerateDerived* 
(dbmanager_wx.genBatchRecords (start_ts - 1, stop_ts)) changed 

3. weewxwd3.py *WdGenerateDerived  *habe a look at weewxwd3.py 

4. I have run the procedere from weew wd

https://bitbucket.org/ozgreg/weewx-wd/src/110688ada0c69c43912a93725976091278c2bb60/?at=v1.2.0_development

    - create the Weewx-WD database and archive table:

        weewxwd_config --create-archive   (((weewxacerWD))))

    - check if there is any legacy data to copy, or if any historical Weewx-WD 
data can be reconstructed:

        weewxwd_config --status

    - copy any legacy data or reconstruct any historical Weewx-WD data:

        weewxwd_config --copy-v2-data    (((from weewxacer to weewxacerWD)))

After 17 hours I had a copy of the weewxacer as weewxacerWD database
the weewxacer database the old
the weewxacerWD one kopie of the old weewxacer-database and the newly 
calculated values 

all TEST TEST TEST
Only for the calculation of about 300 values I will not take my original 
weather database 17 hours offline.

17 hours offline  at the davis should not be a problem 

however the o-wire temperature, water-temperature-sensors does not store any 
data
I am looking for a simple update solution

for example

#!/usr/bin/python
> # -*- coding: utf-8 -*-
> import MySQLdb as mdb
> import sys
> try:
>     conn = mdb.connect('localhost', 'testuser', 
>         'test623', 'testdb');
> For "first Time in weewx-database" to "last Time in weewx-database" step 1:
>     cursor = conn.cursor()
>     cursor.execute("UPDATE archive SET maxSolarRad = %s WHERE dateTime = %s", 
>    ("123.98", "1394092770"))       
>     cursor.execute("UPDATE archive SET windRun = %s WHERE dateTime = %s",   
> ("233.08", "1394092770"))
>     cursor.execute("UPDATE archive SET cdIndex = %s WHERE dateTime = %s",    
> ("45.7", "1394092770"))   
>
>            and so on.. 

>     conn.commit()
>     cursor.close()
>     conn.close()
> """
>
>
 Format-compliant to weewx as a                 updateDB.py


hartmut

# -*- coding: utf-8 -*-
##This program is free software; you can redistribute it and/or modify it under
##the terms of the GNU General Public License as published by the Free Software
##Foundation; either version 2 of the License, or (at your option) any later
##version.
##
##This program is distributed in the hope that it will be useful, but WITHOUT
##ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
##FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
##details.
##
## Version: 1.2.0b1                                    Date: 1 August 2015
##
## Revision History
##  ?? August 2015        v1.2.0    -revised for Weewx v3.2.0
##                                  -moved __main__ code to weewxwd_config utility
##                                  -now uses appTemp and humidex as provided by
##                                   StdWXCalculate
##                                  -simplified WdWXCalculate.new_loop_packet,
##                                   WdWXCalculate.new_archive_record and
##                                   WdArchive.new_archive_record methods
##  10 January 2015     v1.0.0      -rewritten for Weewx v3.0
##                                  -uses separate database for Weewx-WD specific
##                                   data, no longer recycles existing Weewx
##                                   database fields
##                                  -added __main__ to allow command line execution
##                                   of a number of db management actions
##                                  -removed --debug option from main()
##                                  -added --create_archive option to main()
##                                   to create the weewxwd database
##                                  -split --backfill_daily into separate
##                                   --drop_daily and --backfill_daily options
##                                  -added 'user.' to all Weewx-WD imports
##  18 September 2014   v0.9.4      -Added GNU license text
##      (never released)
##  18 May 2014         v0.9.2      -Removed code that set windDir/windGustDir to 0
##                                   if windDir/windGustDir were None respectively
##  30 July 2013        v0.9.1      -Revised version number to align with Weewx-WD
##                                   version numbering
##  20 July 2013        v0.1        -Initial implementation
##

import syslog
import threading
import urllib2
import json
import math
import os
import time
import ephem
from math import sin
from datetime import datetime

import weewx
import weewx.engine
import weewx.manager
import weewx.wxformulas
import weewx.almanac
from weewx.units import convert, obs_group_dict
from weeutil.weeutil import to_bool, accumulateLeaves
from weewx.units import CtoF, CtoK, mps_to_mph, kph_to_mph

WEEWXWD_VERSION = '1.2.0b1'

# Define a dictionary with our API call query details
WU_queries = [
    {
        'name': 'conditions',
        'interval': None,
        'last': None,
        'interval': None,
        'def_interval': 1800,
        'response': None,
        'json_title': 'current_observation'
    },
    {
        'name': 'forecast',
        'interval': None,
        'last': None,
        'interval': 1800,
        'def_interval': 1800,
        'response': None,
        'json_title': 'forecast'
    },
    {
        'name': 'almanac',
        'interval': None,
        'last': None,
        'interval': 3600,
        'def_interval': 3600,
        'response': None,
        'json_title': 'almanac'
    }
]

# Define a dictionary to look up WU icon names and
# return corresponding Saratoga icon code
icon_dict = {
    'clear'             : 0,
    'cloudy'            : 18,
    'flurries'          : 25,
    'fog'               : 11,
    'hazy'              : 7,
    'mostlycloudy'      : 18,
    'mostlysunny'       : 9,
    'partlycloudy'      : 19,
    'partlysunny'       : 9,
    'sleet'             : 23,
    'rain'              : 20,
    'snow'              : 25,
    'sunny'             : 28,
    'tstorms'           : 29,
    'nt_clear'          : 1,
    'nt_cloudy'         : 13,
    'nt_flurries'       : 16,
    'nt_fog'            : 11,
    'nt_hazy'           : 13,
    'nt_mostlycloudy'   : 13,
    'nt_mostlysunny'    : 1,
    'nt_partlycloudy'   : 4,
    'nt_partlysunny'    : 1,
    'nt_sleet'          : 12,
    'nt_rain'           : 14,
    'nt_snow'           : 16,
    'nt_tstorms'        : 17,
    'chancerain'        : 20,
    'chancesleet'       : 23,
    'chancesnow'        : 25,
    'chancetstorms'     : 29
    }


# Define a dictionary to look up Davis forecast rule
# and return forecast text
davis_fr_dict= {
        0   : 'Meist heiter und kälter.',
        1   : 'Meist heiter mit geringer Temperaturänderung.',
        2   : 'Meist heiter für 12 Stunden mit geringer Temperaturänderung.',
        3   : 'Meist heiter für 12 bis 24 Stunden und kälter.',
        4   : 'Meist heiter mit geringer Temperaturänderung.',
        5   : 'Teils wolkig und kälter.',
        6   : 'Teils wolkig mit geringer Temperaturänderung.',
        7   : 'Teils wolkig mit geringer Temperaturänderung.',
        8   : 'Meist heiter und wärmer.',
        9   : 'Teils wolkig mit geringer Temperaturänderung.',
        10  : 'Teils wolkig mit geringer Temperaturänderung.',
        11  : 'Meist heiter mit geringer Temperaturänderung.',
        12  : 'Zunehmend bewölkt und wärmer. Niederschlag möglich innerhalb 24 bis 48 Stunden.',
        13  : 'Teils wolkig mit geringer Temperaturänderung.',
        14  : 'Meist heiter mit geringer Temperaturänderung.',
        15  : 'Zunehmend bewölkt mit geringer Temperaturänderung. Niederschlag möglich innerhalb 24 Stunden.',
        16  : 'Meist heiter mit geringer Temperaturänderung.',
        17  : 'Teils wolkig mit geringer Temperaturänderung.',
        18  : 'Meist heiter mit geringer Temperaturänderung.',
        19  : 'Zunehmend bewölkt mit geringer Temperaturänderung. Niederschlag möglich innerhalb 12 Stunden.',
        20  : 'Meist heiter mit geringer Temperaturänderung.',
        21  : 'Teils wolkig mit geringer Temperaturänderung.',
        22  : 'Meist heiter mit geringer Temperaturänderung.',
        23  : 'Zunehmend bewölkt und wärmer. Niederschlag möglich innerhalb 24 Stunden.',
        24  : 'Meist heiter und wärmer. Zunehmender Wind.',
        25  : 'Teils wolkig mit geringer Temperaturänderung.',
        26  : 'Meist heiter mit geringer Temperaturänderung.',
        27  : 'Zunehmend bewölkt und wärmer. Niederschlag möglich innerhalb 12 Stunden. Zunehmender Wind.',
        28  : 'Meist heiter und wärmer. Zunehmender Wind.',
        29  : 'Zunehmend bewölkt und wärmer.',
        30  : 'Teils wolkig mit geringer Temperaturänderung.',
        31  : 'Meist heiter mit geringer Temperaturänderung.',
        32  : 'Zunehmend bewölkt und wärmer. Niederschlag möglich innerhalb 12 Stunden. Zunehmender Wind.',
        33  : 'Meist heiter und wärmer. Zunehmender Wind.',
        34  : 'Zunehmend bewölkt und wärmer.',
        35  : 'Teils wolkig mit geringer Temperaturänderung.',
        36  : 'Meist heiter mit geringer Temperaturänderung.',
        37  : 'Zunehmend bewölkt und wärmer. Niederschlag möglich innerhalb 12 Stunden. Zunehmender Wind.',
        38  : 'Teils wolkig mit geringer Temperaturänderung.',
        39  : 'Meist heiter mit geringer Temperaturänderung.',
        40  : 'Meist heiter und wärmer. Niederschlag möglich innerhalb 48 Stunden.',
        41  : 'Meist heiter und wärmer.',
        42  : 'Teils wolkig mit geringer Temperaturänderung.',
        43  : 'Meist heiter mit geringer Temperaturänderung.',
        44  : 'Zunehmend bewölkt mit geringer Temperaturänderung. Niederschlag möglich innerhalb 24 bis 48 Stunden.',
        45  : 'Zunehmend bewölkt mit geringer Temperaturänderung.',
        46  : 'Teils wolkig mit geringer Temperaturänderung.',
        47  : 'Meist heiter mit geringer Temperaturänderung.',
        48  : 'Zunehmend bewölkt und wärmer. Niederschlag möglich innerhalb 12 bis 24 Stunden.',
        49  : 'Teils wolkig mit geringer Temperaturänderung.',
        50  : 'Meist heiter mit geringer Temperaturänderung.',
        51  : 'Zunehmend bewölkt und wärmer. Niederschlag möglich innerhalb 12 bis 24 Stunden. Windig.',
        52  : 'Teils wolkig mit geringer Temperaturänderung.',
        53  : 'Meist heiter mit geringer Temperaturänderung.',
        54  : 'Zunehmend bewölkt und wärmer. Niederschlag möglich innerhalb 12 bis 24 Stunden. Windig.',
        55  : 'Teils wolkig mit geringer Temperaturänderung.',
        56  : 'Meist heiter mit geringer Temperaturänderung.',
        57  : 'Zunehmend bewölkt und wärmer. Niederschlag möglich innerhalb 6 bis 12 Stunden.',
        58  : 'Teils wolkig mit geringer Temperaturänderung.',
        59  : 'Meist heiter mit geringer Temperaturänderung.',
        60  : 'Zunehmend bewölkt und wärmer. Niederschlag möglich innerhalb 6 bis 12 Stunden. Windig.',
        61  : 'Teils wolkig mit geringer Temperaturänderung.',
        62  : 'Meist heiter mit geringer Temperaturänderung.',
        63  : 'Zunehmend bewölkt und wärmer. Niederschlag möglich innerhalb 12 bis 24 Stunden. Windig.',
        64  : 'Teils wolkig mit geringer Temperaturänderung.',
        65  : 'Meist heiter mit geringer Temperaturänderung.',
        66  : 'Zunehmend bewölkt und wärmer. Niederschlag möglich innerhalb 12 Stunden.',
        67  : 'Teils wolkig mit geringer Temperaturänderung.',
        68  : 'Meist heiter mit geringer Temperaturänderung.',
        69  : 'Zunehmend bewölkt und wärmer. Niederschlag wahrscheinlich.',
        70  : 'Aufklarend und kälter. Niederschlag endet innerhalb 6 Stunden.',
        71  : 'Teils wolkig mit geringer Temperaturänderung.',
        72  : 'Aufklarend und kälter. Niederschlag endet innerhalb 6 Stunden.',
        73  : 'Meist heiter mit geringer Temperaturänderung.',
        74  : 'Aufklarend und kälter. Niederschlag endet innerhalb 6 Stunden.',
        75  : 'Teils wolkig und kälter.',
        76  : 'Teils wolkig mit geringer Temperaturänderung.',
        77  : 'Meist heiter und kälter.',
        78  : 'Aufklarend und kälter. Niederschlag endet innerhalb 6 Stunden.',
        79  : 'Meist heiter mit geringer Temperaturänderung.',
        80  : 'Aufklarend und kälter. Niederschlag endet innerhalb 6 Stunden.',
        81  : 'Meist heiter und kälter.',
        82  : 'Teils wolkig mit geringer Temperaturänderung.',
        83  : 'Meist heiter mit geringer Temperaturänderung.',
        84  : 'Zunehmend bewölkt mit geringer Temperaturänderung. Niederschlag möglich innerhalb 24 Stunden.',
        85  : 'Meist wolkig und kälter. Niederschlag dauert an.',
        86  : 'Teils wolkig mit geringer Temperaturänderung.',
        87  : 'Meist heiter mit geringer Temperaturänderung.',
        88  : 'Meist wolkig und kälter. Niederschlag wahrscheinlich.',
        89  : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag dauert an.',
        90  : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag wahrscheinlich.',
        91  : 'Teils wolkig mit geringer Temperaturänderung.',
        92  : 'Meist heiter mit geringer Temperaturänderung.',
        93  : 'Zunehmend bewölkt und kälter. Niederschlag möglich und windig innerhalb 6 Stunden.',
        94  : 'Zunehmend bewölkt mit geringer Temperaturänderung. Niederschlag möglich und windig innerhalb 6 Stunden.',
        95  : 'Meist wolkig und kälter. Niederschlag dauert an. Zunehmender Wind.',
        96  : 'Teils wolkig mit geringer Temperaturänderung.',
        97  : 'Meist heiter mit geringer Temperaturänderung.',
        98  : 'Meist wolkig und kälter. Niederschlag wahrscheinlich. Zunehmender Wind.',
        99  : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag dauert an. Zunehmender Wind.',
        100 : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag wahrscheinlich. Zunehmender Wind.',
        101 : 'Teils wolkig mit geringer Temperaturänderung.',
        102 : 'Meist heiter mit geringer Temperaturänderung.',
        103 : 'Zunehmend bewölkt und kälter. Niederschlag möglich innerhalb 12 bis 24 Stunden und möglicher Windrichtungswechsel auf W, NW, oder N.',
        104 : 'Zunehmend bewölkt mit geringer Temperaturänderung. Niederschlag möglich innerhalb 12 bis 24 Stunden und möglicher Windrichtungswechsel auf W, NW, oder N.',
        105 : 'Teils wolkig mit geringer Temperaturänderung.',
        106 : 'Meist heiter mit geringer Temperaturänderung.',
        107 : 'Zunehmend bewölkt und kälter. Niederschlag möglich innerhalb 6 Stunden und möglicher Windrichtungswechsel auf W, NW, oder N.',
        108 : 'Zunehmend bewölkt mit geringer Temperaturänderung. Niederschlag möglich innerhalb 6 Stunden und möglicher Windrichtungswechsel auf W, NW, oder N.',
        109 : 'Meist wolkig und kälter. Niederschlag endet innerhalb 12 Stunden und möglicher Windrichtungswechsel auf W, NW, oder N.',
        110 : 'Meist wolkig und kälter. Windrichtungswechsel möglich auf W, NW, oder N.',
        111 : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag endet innerhalb 12 Stunden und möglicher Windrichtungswechsel auf W, NW, oder N.',
        112 : 'Meist wolkig mit geringer Temperaturänderung. Windrichtungswechsel möglich auf W, NW, oder N.',
        113 : 'Meist wolkig und kälter. Niederschlag endet innerhalb 12 Stunden und möglicher Windrichtungswechsel auf W, NW, oder N.',
        114 : 'Teils wolkig mit geringer Temperaturänderung.',
        115 : 'Meist heiter mit geringer Temperaturänderung.',
        116 : 'Meist wolkig und kälter. Niederschlag möglich innerhalb 24 Stunden und möglicher Windrichtungswechsel auf W, NW, oder N.',
        117 : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag endet innerhalb 12 Stunden und möglicher Windrichtungswechsel auf W, NW, oder N.',
        118 : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag möglich innerhalb 24 Stunden und möglicher Windrichtungswechsel auf W, NW, oder N.',
        119 : 'Aufklarend, kälter und windig. Niederschlag endet innerhalb 6 Stunden.',
        120 : 'Aufklarend, kälter und windig.',
        121 : 'Meist wolkig und kälter. Niederschlag endet innerhalb 6 Stunden. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        122 : 'Meist wolkig und kälter. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        123 : 'Aufklarend, kälter und windig.',
        124 : 'Teils wolkig mit geringer Temperaturänderung.',
        125 : 'Meist heiter mit geringer Temperaturänderung.',
        126 : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag möglich innerhalb 12 Stunden. Windig.',
        127 : 'Teils wolkig mit geringer Temperaturänderung.',
        128 : 'Meist heiter mit geringer Temperaturänderung.',
        129 : 'Zunehmend bewölkt und kälter. Niederschlag möglich innerhalb 12 Stunden, zeitweise heftig. Windig.',
        130 : 'Meist wolkig und kälter. Niederschlag endet innerhalb 6 Stunden. Windig.',
        131 : 'Teils wolkig mit geringer Temperaturänderung.',
        132 : 'Meist heiter mit geringer Temperaturänderung.',
        133 : 'Meist wolkig und kälter. Niederschlag möglich innerhalb 12 Stunden. Windig.',
        134 : 'Meist wolkig und kälter. Niederschlag endet in 12 bis 24 Stunden.',
        135 : 'Meist wolkig und kälter.',
        136 : 'Meist wolkig und kälter. Niederschlag dauert an, zeitweise heftig. Windig.',
        137 : 'Teils wolkig mit geringer Temperaturänderung.',
        138 : 'Meist heiter mit geringer Temperaturänderung.',
        139 : 'Meist wolkig und kälter. Niederschlag möglich innerhalb 6 bis 12 Stunden. Windig.',
        140 : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag dauert an, zeitweise heftig. Windig.',
        141 : 'Teils wolkig mit geringer Temperaturänderung.',
        142 : 'Meist heiter mit geringer Temperaturänderung.',
        143 : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag möglich innerhalb 6 bis 12 Stunden. Windig.',
        144 : 'Teils wolkig mit geringer Temperaturänderung.',
        145 : 'Meist heiter mit geringer Temperaturänderung.',
        146 : 'Zunehmend bewölkt mit geringer Temperaturänderung. Niederschlag möglich innerhalb 12 Stunden, zeitweise heftig. Windig.',
        147 : 'Meist wolkig und kälter. Windig.',
        148 : 'Meist wolkig und kälter. Niederschlag dauert an, zeitweise heftig. Windig.',
        149 : 'Teils wolkig mit geringer Temperaturänderung.',
        150 : 'Meist heiter mit geringer Temperaturänderung.',
        151 : 'Meist wolkig und kälter. Niederschlag wahrscheinlich, zeitweise heftig. Windig.',
        152 : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag dauert an, zeitweise heftig. Windig.',
        153 : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag wahrscheinlich, zeitweise heftig. Windig.',
        154 : 'Teils wolkig mit geringer Temperaturänderung.',
        155 : 'Meist heiter mit geringer Temperaturänderung.',
        156 : 'Zunehmend bewölkt und kälter. Niederschlag möglich innerhalb 6 Stunden. Windig.',
        157 : 'Zunehmend bewölkt mit geringer Temperaturänderung. Niederschlag möglich innerhalb 6 Stunden. Windig.',
        158 : 'Zunehmend bewölkt und kälter. Niederschlag dauert an. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        159 : 'Teils wolkig mit geringer Temperaturänderung.',
        160 : 'Meist heiter mit geringer Temperaturänderung.',
        161 : 'Meist wolkig und kälter. Niederschlag wahrscheinlich. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        162 : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag dauert an. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        163 : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag wahrscheinlich. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        164 : 'Zunehmend bewölkt und kälter. Niederschlag möglich innerhalb 6 Stunden. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        165 : 'Teils wolkig mit geringer Temperaturänderung.',
        166 : 'Meist heiter mit geringer Temperaturänderung.',
        167 : 'Zunehmend bewölkt und kälter. Niederschlag möglich innerhalb 6 Stunden und möglicher Windrichtungswechsel auf W, NW, oder N.',
        168 : 'Zunehmend bewölkt mit geringer Temperaturänderung. Niederschlag möglich innerhalb 6 Stunden. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        169 : 'Zunehmend bewölkt mit geringer Temperaturänderung. Niederschlag möglich innerhalb 6 Stunden und möglicher Windrichtungswechsel auf W, NW, oder N.',
        170 : 'Teils wolkig mit geringer Temperaturänderung.',
        171 : 'Meist heiter mit geringer Temperaturänderung.',
        172 : 'Zunehmend bewölkt und kälter. Niederschlag möglich innerhalb 6 Stunden. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        173 : 'Zunehmend bewölkt mit geringer Temperaturänderung. Niederschlag möglich innerhalb 6 Stunden. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        174 : 'Teils wolkig mit geringer Temperaturänderung.',
        175 : 'Meist heiter mit geringer Temperaturänderung.',
        176 : 'Zunehmend bewölkt und kälter. Niederschlag möglich innerhalb 12 bis 24 Stunden. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        177 : 'Zunehmend bewölkt mit geringer Temperaturänderung. Niederschlag möglich innerhalb 12 bis 24 Stunden. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        178 : 'Meist wolkig und kälter. Niederschlag zeitweise heftig und endet innerhalb 12 Stunden. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        179 : 'Teils wolkig mit geringer Temperaturänderung.',
        180 : 'Meist heiter mit geringer Temperaturänderung.',
        181 : 'Meist wolkig und kälter. Niederschlag möglich innerhalb 6 bis 12 Stunden, zeitweise heftig. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        182 : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag endet innerhalb 12 Stunden. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        183 : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag möglich innerhalb 6 bis 12 Stunden, zeitweise heftig. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        184 : 'Meist wolkig und kälter. Niederschlag dauert an.',
        185 : 'Teils wolkig mit geringer Temperaturänderung.',
        186 : 'Meist heiter mit geringer Temperaturänderung.',
        187 : 'Meist wolkig und kälter. Niederschlag wahrscheinlich. Windig mit möglichem Windrichtungswechsel auf W, NW, oder N.',
        188 : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag dauert an.',
        189 : 'Meist wolkig mit geringer Temperaturänderung. Niederschlag wahrscheinlich.',
        190 : 'Teils wolkig mit geringer Temperaturänderung.',
        191 : 'Meist heiter mit geringer Temperaturänderung.',
        192 : 'Meist wolkig und kälter. Niederschlag möglich innerhalb 12 Stunden, zeitweise heftig. Windig.',
        193 : 'FORECAST REQUIRES 3 HOURS OF RECENT DATA',
        194 : 'Meist heiter und kälter.',
        195 : 'Meist heiter und kälter.',
        196 : 'Meist heiter und kälter.',        
        }

def logmsg(level, src, msg):
    syslog.syslog(level, '%s %s' % (src, msg))

def logdbg(src, msg):
    logmsg(syslog.LOG_DEBUG, src, msg)

def logdbg2(src, msg):
    if weewx.debug >= 2:
        logmsg(syslog.LOG_DEBUG, src, msg)

def loginf(src, msg):
    logmsg(syslog.LOG_INFO, src, msg)

def logerr(src, msg):
    logmsg(syslog.LOG_ERR, src, msg)

#===============================================================================
#                            Class WdWXCalculate
#===============================================================================

class WdWXCalculate(weewx.engine.StdService):

    def __init__(self, engine, config_dict):
        super(WdWXCalculate, self).__init__(engine, config_dict)

        # bind ourself to both loop and archive events
        self.bind(weewx.NEW_LOOP_PACKET, self.new_loop_packet)
        self.bind(weewx.NEW_ARCHIVE_RECORD, self.new_archive_record)

    def new_loop_packet(self, event):
        data_x = {}
        if 'outTemp' in event.packet:
            data_x['outTempDay'], data_x['outTempNight'] = calc_daynighttemps(event.packet['outTemp'], event.packet['dateTime'])
        else:
            data_x['outTempDay'], data_x['outTempNight'] = (None, None)

        event.packet.update(data_x)

    def new_archive_record(self, event):
        data_x = {}
        if 'outTemp' in event.record:
            data_x['outTempDay'], data_x['outTempNight'] = calc_daynighttemps(event.record['outTemp'], event.record['dateTime'])
        else:
            data_x['outTempDay'], data_x['outTempNight'] = (None, None)


        event.record.update(data_x)

#===============================================================================
#                              Class WdArchive
#===============================================================================

class WdArchive(weewx.engine.StdService):
    """ Service to store Weewx-WD specific archive data. """

    def __init__(self, engine, config_dict):
        super(WdArchive, self).__init__(engine, config_dict)

        # Extract our binding from the Weewx-WD section of the config file. If
        # it's missing, fill with a default
        if 'WeewxWD' in config_dict:
            self.data_binding = config_dict['WeewxWD'].get('data_binding', 'wd_binding')
        else:
            self.data_binding = 'wd_binding'

        # Extract the Weewx binding for use when we check the need for backfill
        # from the Weewx archive
        if 'StdArchive' in config_dict:
            self.data_binding_wx = config_dict['StdArchive'].get('data_binding', 'wx_binding')
        else:
            self.data_binding_wx = 'wx_binding'

        loginf("WdArchive:", "WdArchive will use data binding %s" % self.data_binding)

        # setup our database if needed
        self.setup_database(config_dict)

        # set the unit groups for our obs
        obs_group_dict["humidex"] = "group_temperature"
        obs_group_dict["appTemp"] = "group_temperature"
        obs_group_dict["outTempDay"] = "group_temperature"
        obs_group_dict["outTempNight"] = "group_temperature"

        # bind ourselves to NEW_ARCHIVE_RECORD event
        self.bind(weewx.NEW_ARCHIVE_RECORD, self.new_archive_record)


    def new_archive_record(self, event):
        """Called when a new archive record has arrived.

           Use our manager's addRecord method to save the relevant Weewx-WD
           fields to Weewx-WD archive.
        """

        # get our manager
        dbmanager = self.engine.db_binder.get_manager(self.data_binding)
        # now put the record in the archive
        dbmanager.addRecord(event.record)

    def setup_database(self, config_dict):
        """Setup the main database archive"""

        # This will create the database if it doesn't exist, then return an
        # opened instance of the database manager.
        dbmanager = self.engine.db_binder.get_manager(self.data_binding, initialize=True)
        loginf("WdArchive:", "Using binding '%s' to database '%s'" % (self.data_binding, dbmanager.database_name))

        # Check if we have any historical data to suck in from Weewx main archive
        # get a dbmanager for the Weewx archive
        dbmanager_wx = self.engine.db_binder.get_manager(self.data_binding_wx, initialize=False)


        # Back fill the daily summaries.
        loginf("WdArchive:", "Starting backfill of daily summaries")
        t1 = time.time()
        nrecs, ndays = dbmanager.backfill_day_summary()
        tdiff = time.time() - t1
        if nrecs:
            loginf("WdArchive:", "Processed %d records to backfill %d day summaries in %.2f seconds" % (nrecs, ndays, tdiff))
        else:
            loginf("WdArchive:", "Daily summaries up to date.")

#===============================================================================
#                           Class WdGenerateDerived
#===============================================================================

class WdGenerateDerived(object):
    """ Generator wrapper. Adds Weewx-WD derived obs to the output of the
        wrapped generator.
    """

    def __init__(self, input_generator):
        """ Initialize an instance of WdGenerateDerived

            input_generator: An iterator which will return dictionary records.
        """
        self.input_generator = input_generator

    def __iter__(self):
        return self

    def next(self):
        # get our next record
        _rec = self.input_generator.next()
        _rec_mwx = weewx.units.to_METRIC(_rec)

        _rec_mwx['dateTime'] = _rec_mwx['dateTime'] 
        _rec_mwx['usUnits'] = _rec_mwx['usUnits'] 
        _rec_mwx['interval'] = _rec_mwx['interval']
        _rec_mwx['barometer'] = _rec_mwx['barometer'] 
        _rec_mwx['pressure'] =  _rec_mwx['pressure'] 
        _rec_mwx['altimeter'] = _rec_mwx['altimeter']
        _rec_mwx['inTemp'] = _rec_mwx['inTemp']
        _rec_mwx['outTemp'] =  _rec_mwx['outTemp']  
        _rec_mwx['inHumidity'] = _rec_mwx['inHumidity']
        _rec_mwx['outHumidity'] = _rec_mwx['outHumidity']
        _rec_mwx['windSpeed'] = _rec_mwx['windSpeed']
        _rec_mwx['windDir'] = _rec_mwx['windDir']   
        _rec_mwx['windGust'] = _rec_mwx['windGust']  
        _rec_mwx['windGustDir'] = _rec_mwx['windGustDir']
        _rec_mwx['rainRate'] = _rec_mwx['rainRate']  
        _rec_mwx['rain'] = _rec_mwx['rain']
        _rec_mwx['heatindex'] = _rec_mwx['heatindex']
        _rec_mwx['ET'] =  _rec_mwx['ET']
        _rec_mwx['radiation'] = _rec_mwx['radiation']
        _rec_mwx['UV'] = _rec_mwx['UV']
        _rec_mwx['extraTemp1'] = _rec_mwx['extraTemp1']
        _rec_mwx['extraTemp2'] = _rec_mwx['extraTemp2']
        _rec_mwx['extraTemp3'] = _rec_mwx['extraTemp3']
        _rec_mwx['soilTempO1'] = _rec_mwx['soilTempO1']
        _rec_mwx['soilTempO2'] = _rec_mwx['soilTempO2']
        _rec_mwx['soilTempO3'] = _rec_mwx['soilTempO3']
        _rec_mwx['soilTempO4'] = _rec_mwx['soilTempO4']
        _rec_mwx['soilTempO5'] = _rec_mwx['soilTempO5']
        _rec_mwx['leafTemp1'] = _rec_mwx['leafTemp1']
        _rec_mwx['leafTemp2'] = _rec_mwx['leafTemp2']
        _rec_mwx['extraHumid1'] = _rec_mwx['extraHumid1']
        _rec_mwx['extraHumid2'] = _rec_mwx['extraHumid2']
        _rec_mwx['soilMoist1'] = _rec_mwx['soilMoist1']
        _rec_mwx['soilMoist2'] = _rec_mwx['soilMoist2']
        _rec_mwx['soilMoist3'] = _rec_mwx['soilMoist3']
        _rec_mwx['soilMoist4'] = _rec_mwx['soilMoist4']
        _rec_mwx['leafWet1'] = _rec_mwx['leafWet1']
        _rec_mwx['leafWet2'] = _rec_mwx['leafWet2']
        _rec_mwx['rxCheckPercent'] = _rec_mwx['rxCheckPercent']
        _rec_mwx['txBatteryStatus'] = _rec_mwx['txBatteryStatus']
        _rec_mwx['consBatteryVoltage'] = _rec_mwx['consBatteryVoltage']
        _rec_mwx['hail'] = _rec_mwx['hail']
        _rec_mwx['hailRate'] = _rec_mwx['hailRate']
        _rec_mwx['heatingTemp'] = _rec_mwx['heatingTemp']
        _rec_mwx['heatingVoltage'] = _rec_mwx['heatingVoltage'] 
        _rec_mwx['supplyVoltage'] = _rec_mwx['supplyVoltage']
        _rec_mwx['referenceVoltage'] = _rec_mwx['referenceVoltage']
        _rec_mwx['windBatteryStatus'] = _rec_mwx['windBatteryStatus']
        _rec_mwx['rainBatteryStatus'] = _rec_mwx['rainBatteryStatus']
        #_rec_mwx['outTempBatteryStatus'] = _rec_mwx['outTempBatteryStatus']
        _rec_mwx['lighting'] = _rec_mwx['lighting']
        _rec_mwx['extraTemp4'] = _rec_mwx['extraTemp4']
        _rec_mwx['extraTemp5'] = _rec_mwx['extraTemp5']
        _rec_mwx['extraTemp6'] = _rec_mwx['extraTemp6']
        _rec_mwx['extraTemp7'] = _rec_mwx['extraTemp7']
        _rec_mwx['extraTemp8'] = _rec_mwx['extraTemp8']
        _rec_mwx['extraTemp9'] = _rec_mwx['extraTemp9']
        #_rec_mwx['maxSolarRad'] = _rec_mwx['maxSolarRad']
        #_rec_mwx['cloudbase'] = _rec_mwx['cloudbase']

        _rec_mwx['maxSolarRad'] = weewx.wxformulas.solar_rad_Bras(53.605963, 11.341407, 53, _rec_mwx['dateTime'], 2)

        if 'outTemp' in _rec_mwx and 'outHumidity' in _rec_mwx:
            _rec_mwx['cloudbase'] = weewx.wxformulas.cloudbase_Metric(_rec_mwx['outTemp'], _rec_mwx['outHumidity'], 53)
        else:
            _rec_mwx['cloudbase'] = None

        if 'windSpeed' in _rec_mwx and 'windSpeed' > 0.0: # _rec_mwx['windSpeed'] > 0.0:
            _rec_mwx['windrun'] = _rec_mwx['windSpeed'] * 5.0 / 60.0
        else:
            _rec_mwx['windrun'] = 0.0

        #_rec_mwx['inDewpoint'] = _rec_mwx['inDewpoint']
        if 'inTemp' in _rec_mwx and 'inHumidity' in _rec_mwx:
            _rec_mwx['inDewpoint'] = weewx.wxformulas.dewpointC(_rec_mwx['inTemp'], _rec_mwx['inHumidity'])
        else:
            _rec_mwx['inDewpoint'] = None

        #_rec_mwx['inTempBatteryStatus'] = _rec_mwx['inTempBatteryStatus']
        #_rec_mwx['sunshineS'] = _rec_mwx['sunshineS']
        if 'radiation' in _rec_mwx:
           _rec_mwx['sunshineS'] = weewx.wxformulas.sunhes(_rec_mwx['radiation'], _rec_mwx['dateTime'])
           #_rec_mwx['sunshinehours'] = _rec_mwx['sunshineS'] / 3600.0
        else:
           _rec_mwx['sunshineS'] = None
           #_rec_mwx['sunshinehours'] = None
        _rec_mwx['snow'] = _rec_mwx['snow']
        _rec_mwx['snowRate'] = _rec_mwx['snowRate']
        _rec_mwx['snowTotal'] = _rec_mwx['snowTotal']
        _rec_mwx['soilTemp1'] = _rec_mwx['soilTemp1']
        _rec_mwx['soilTemp2'] = _rec_mwx['soilTemp2']
        _rec_mwx['soilTemp3'] = _rec_mwx['soilTemp3']
        _rec_mwx['soilTemp4'] = _rec_mwx['soilTemp4'] 
        _rec_mwx['soilTemp5'] = _rec_mwx['soilTemp5'] 

        _rec_mwx['windchill'] =  weewx.wxformulas.windchillC(_rec_mwx['outTemp'], _rec_mwx['windSpeed'])
        _rec_mwx['heatindex'] =  weewx.wxformulas.heatindexC(_rec_mwx['outTemp'], _rec_mwx['outHumidity'])
        _rec_mwx['humidex'] = weewx.wxformulas.humidexC(_rec_mwx['outTemp'], _rec_mwx['outHumidity'])
        _rec_mwx['appTemp']  = weewx.wxformulas.apptempC(_rec_mwx['outTemp'], _rec_mwx['outHumidity'], _rec_mwx['windSpeed'])
        _rec_mwx['airDensity'] = weewx.wxformulas.density_US(_rec_mwx['dewpoint'], _rec_mwx['outTemp'], _rec_mwx['pressure'])
        _rec_mwx['windDruck'] = weewx.wxformulas.winddruck_US(_rec_mwx['dewpoint'], _rec_mwx['outTemp'], _rec_mwx['pressure'], _rec_mwx['windSpeed'])

        if 'outTemp' in _rec_mwx:
            _rec_mwx['outTempDay'], _rec_mwx['outTempNight'] = weewx.wxformulas.daynighttempC(_rec_mwx['outTemp'], _rec_mwx['dateTime'])
        else:
            _rec_mwx['outTempDay'], _rec_mwx['outTempNight'] = (None, None)

        data_x = weewx.units.to_std_system(_rec_mwx, _rec_mwx['usUnits'])
        # return our modified record
        return data_x

#===============================================================================
#                             Class wdSuppThread
#===============================================================================

class wdSuppThread(threading.Thread):
    """ Thread in which to run WdSuppArchive service.

        As we need to obtain WU data via WU API query we need to run this in
        another thread so as to not hold up Weewx if we have a slow connection
        or WU is unresponsive for any reason.
    """

    def __init__(self, target, *args):
        self._target = target
        self._args = args
        threading.Thread.__init__(self)

    def run(self):
        self._target(*self._args)

#===============================================================================
#                            Class WdSuppArchive
#===============================================================================

class WdSuppArchive(weewx.engine.StdService):
    """ Service to obtain and archive WU API sourced data and Davis console
        forecast/storm data as well as archive theoretical max solar
        radiation data. Data is only kept for a limited time before being
        dropped.
    """

    def __init__(self, engine, config_dict):
        super(WdSuppArchive, self).__init__(engine, config_dict)

        # Initialisation is 2 part; 1 part for wdsupp db/loop data, 2nd part
        # for WU API calls. We are only going to invoke ourself if we have the
        # necessary config data available in weewx.conf for 1 or both parts.
        # If any essential config data is missing/not set then give a short log
        # message and defer.

        if 'Weewx-WD' in config_dict:
            # we have a [Weewx-WD} stanza
            if 'Supplementary' in config_dict['Weewx-WD']:
                # we have a [[Supplementary]] stanza so we can initialise wdsupp db
                # get our binding, if it's missing use a default
                self.binding = config_dict['Weewx-WD']['Supplementary'].get('data_binding', 'wdsupp_binding')
                loginf("WdSuppArchive:", "WdSuppArchive will use data binding '%s'" % self.binding)
                # how long to keep records in our db (default 8 days)
                self.max_age = config_dict['Weewx-WD']['Supplementary'].get('max_age', 691200)
                self.max_age = toint('max_age', self.max_age, 691200)
                # how often to vacuum the sqlite database (default 24 hours)
                self.vacuum = config_dict['Weewx-WD']['Supplementary'].get('vacuum', 86400)
                self.vacuum = toint('vacuum', self.vacuum, 86400)
                # how many times do we retry database failures (default 3)
                self.db_max_tries = config_dict['Weewx-WD']['Supplementary'].get('database_max_tries', 3)
                self.db_max_tries = int(self.db_max_tries)
                # how long to wait between retries (default 2 sec)
                self.db_retry_wait = config_dict['Weewx-WD']['Supplementary'].get('database_retry_wait', 2)
                self.db_retry_wait = int(self.db_retry_wait)

                # initialise a few things
                # setup our database if needed
                self.setup_database(config_dict)
                # ts at which we last vacuumed
                self.last_vacuum = None
                # create holder for Davis Console loop data
                self.loop_packet = {}

                # set the unit groups for our obs
                obs_group_dict["tempRecordHigh"] = "group_temperature"
                obs_group_dict["tempNormalHigh"] = "group_temperature"
                obs_group_dict["tempRecordLow"] = "group_temperature"
                obs_group_dict["tempNormalLow"] = "group_temperature"
                obs_group_dict["tempRecordHighYear"] = "group_count"
                obs_group_dict["tempRecordLowYear"] = "group_count"
                obs_group_dict["stormRain"] = "group_rain"
                obs_group_dict["stormStart"] = "group_time"
                obs_group_dict["maxSolarRad"] = "group_radiation"
                obs_group_dict["forecastIcon"] = "group_count"
                obs_group_dict["currentIcon"] = "group_count"
                obs_group_dict["vantageForecastIcon"] = "group_count"
                obs_group_dict["visibility_km"] = "group_distance"
                obs_group_dict["pop"] = "group_count"
                obs_group_dict["vantageForecastNumber"] = "group_count"

                # event bindings
                # bind to NEW_LOOP_PACKET so we can capture Davis Vantage forecast
                # data
                self.bind(weewx.NEW_LOOP_PACKET, self.new_loop_packet)

                # bind to NEW_ARCHIVE_RECORD to ensure we have a chance to:
                # - update WU data(if necessary)
                # - save our data
                # on each new record
                self.bind(weewx.NEW_ARCHIVE_RECORD, self.new_archive_record)

            # do we have necessary config info for WU ie a [[[WU]]] stanza,
            # apiKey and location
            _wu_dict = check_enable(config_dict['Weewx-WD']['Supplementary'], 'WU', 'apiKey')
            if _wu_dict is None:
                # we are missing some/all essential WU API settings so set a
                # flag and return
                self.do_WU = False
                loginf("WdSuppArchive:", "WU API calls will not be made")
                loginf("              ", "**** Incomplete or missing config settings")
                return

            # if we got this far we have the essential WU API settings so carry
            # on with the rest of the initialisation
            # set a flag indicating we are doing WU API queries
            self.do_WU = True
            # get station info required for almanac/Sun related calcs
            self.latitude = float(config_dict['Station']['latitude'])
            self.longitude = float(config_dict['Station']['longitude'])
            self.altitude = convert(engine.stn_info.altitude_vt, 'meter')[0]
            # create a list of the WU API calls we need
            self.WU_queries = WU_queries
            # set interval between API calls for each API call type we need
            for q in self.WU_queries:
                q['interval'] = int(config_dict['Weewx-WD']['Supplementary']['WU'].get(q['name'] + '_interval', q['def_interval']))
            # Set max no of tries we will make in any one attempt to contact WU via API
            self.max_WU_tries = config_dict['Weewx-WD']['Supplementary']['WU'].get('max_WU_tries', 3)
            self.max_WU_tries = toint('max_WU_tries', self.max_WU_tries, 3)
            # set API call lockout period in sec (default 60 sec). refer weewx.conf
            self.api_lockout_period = config_dict['Weewx-WD']['Supplementary']['WU'].get('api_lockout_period', 60)
            self.api_lockout_period = toint('api_lockout_period', self.api_lockout_period, 60)
            # create holder for last WU API call ts
            self.last_WU_query = None
            # Get our API key from weewx.conf, we know we have something in
            # [Weewx-WD] but it could be None. If this is the case try
            # [Forecast] if it exists. [Weewx-WD] should not throw an exception
            # but [Forecast] may so wrap in a try..except loop to catch any
            # exceptions (eg [Forecast][WU]apiKey does not exist).
            try:
                if config_dict['Weewx-WD']['Supplementary']['WU'].get('apiKey', None) != None:
                    self.api_key = config_dict['Weewx-WD']['Supplementary']['WU'].get('apiKey')
                elif config_dict['Forecast']['WU'].get('api_key', None) != None:
                    self.api_key = config_dict['Forecast']['WU'].get('api_key')
                else:
                    loginf("WdSuppArchive:", "Cannot find valid Weather Underground API key")
                    loginf("              ", "**** Incomplete or missing config settings")
            except:
                loginf("WdSuppArchive:", "Cannot find Weather Underground API key")
                loginf("              ", "**** Incomplete or missing config settings")
            # get our 'location' for use in WU API calls. Default to station
            # lat/long.
            self.location = config_dict['Weewx-WD']['Supplementary']['WU'].get('location', '%s,%s' % (self.latitude, self.longitude))
            if self.location == 'replace_me':
                self.location = '%s,%s' % (self.latitude, self.longitude)
            # set fixed part of WU API call url
            self.default_url = 'http://api.wunderground.com/api'
            # we have everything we need to put a short message in the log with
            # a partially masked API key
            loginf("WdSuppArchive:", 'max_age=%s vacuum=%s, WU API calls will be made' % (self.max_age, self.vacuum))
            _msg = ''
            for _wuq in self.WU_queries:
                _msg += _wuq['name'] + ' interval=' + '%s ' % (_wuq['interval'],)
            loginf("WdSuppArchive:", _msg)
            loginf("WdSuppArchive:", 'api_key=%s location=%s' % ('xxxxxxxxxxxx' + self.api_key[-4:], self.location))

    def new_archive_record(self, event):
        """ Kick off in a new thread.
        """

        t = wdSuppThread(self.wdSupp_main, event)
        t.setName('wdSuppThread')
        t.start()

    def wdSupp_main(self, event):
        """ Take care of getting our data, archiving it and completing any
            database housekeeping.

            If we are making WU API calls then step through each of our WU API
            calls, obtain and parse our results. Grab any forecast/storm loop
            data and theoretical max solar radiation> Archive our data, delete
            any stale records and 'vacuum' the database if required.
        """

        # get time now as a ts
        now = time.time()
        # create a holder dict for our data record
        _data_record = {}
        # prepopulate it with a few things we know now
        _data_record['dateTime'] = event.record['dateTime']
        _data_record['usUnits'] = event.record['usUnits']
        _data_record['interval'] = event.record['interval']

        # get our WU data if we are setup for WU
        if self.do_WU:
            # get ts of record about to be processed
            rec_ts = event.record['dateTime']
            # almanac gives more accurate results with current temp and pressure
            # initialise some defaults
            temperature_C = 15.0
            pressure_mbar = 1010.0
            # get current outTemp and barometer if they exist
            if 'outTemp' in event.record:
                temperature_C = weewx.units.convert(weewx.units.as_value_tuple(event.record, 'outTemp'),
                                                    "degree_C")[0]
            if 'barometer' in event.record:
                pressure_mbar = weewx.units.convert(weewx.units.as_value_tuple(event.record, 'barometer'),
                                                    "mbar")[0]
            # get our almanac object
            self.almanac = weewx.almanac.Almanac(rec_ts, self.latitude,
                                                 self.longitude,
                                                 self.altitude,
                                                 temperature_C,
                                                 pressure_mbar)
            # work out sunrise and sunset ts so we can determine if it is night or day. Needed so
            # we can set day or night icons when translating WU icons to Saratoga icons
            sunrise_ts = self.almanac.sun.rise.raw
            sunset_ts = self.almanac.sun.set.raw
            # if we are not between sunrise and sunset it must be night
            self.night = not (sunrise_ts < rec_ts < sunset_ts)
            # get the fully constructed URL for those API feature calls that
            # are to be made
            _WU_URL, _features = self.construct_WU_URL(now)
            _response = None
            if _WU_URL is not None:
                if self.last_WU_query is None or ((now + 1 - self.api_lockout_period) >= self.last_WU_query):
                    # if we haven't made this API call previously or if its been too long since
                    # the last call then make the call, wrap in a try..except just in case
                    try:
                        _response = self.get_WU_response(_WU_URL,
                                                         self.max_WU_tries)
                        logdbg2("WdSuppArchive:", "Downloaded updated Weather Underground information for %s" % (_features,))
                        loginf("WdSuppArchive:", "Downloaded updated Weather Underground information for %s" % (_features,))
                    except Exception, e:
                        loginf("WdSuppArchive:", "Weather Underground API query failure: %s" % (e, ))
                    self.last_WU_query = max(q['last'] for q in self.WU_queries)
                else:
                    # API call limiter kicked in so say so
                    loginf("WdSuppArchive:", "API call limit reached. Tried to make an API call within %d sec of the previous call. API call skipped." % (self.api_lockout_period, ))
            # parse the WU responses and put into a dictionary
            _wu_record = self.parse_WU_response(_response, _data_record['usUnits'])
            # update our data record with any WU data
            _data_record.update(_wu_record)

        # process data from latest loop packet
        _data_loop = self.process_loop()
        # update our data record with any loop data
        _data_record.update(_data_loop)

        # get a dictionary for our database manager
        dbm_dict = weewx.manager.get_manager_dict_from_config(self.config_dict,
                                                              self.binding)
        with weewx.manager.open_manager(dbm_dict) as dbm:
            # save our data
            self.save_record(dbm, _data_record, self.db_max_tries, self.db_retry_wait)
            # set ts of last packet processed
            self.last_ts = _data_record['dateTime']
            # prune older packets and vacuum if required
            if self.max_age > 0:
                self.prune(dbm, self.last_ts - self.max_age,
                           self.db_max_tries,
                           self.db_retry_wait)
                # vacuum the database
                if self.vacuum > 0:
                    if self.last_vacuum is None or ((now + 1 - self.vacuum) >= self.last_vacuum):
                        self.vacuum_database(dbm)
                        self.last_vacuum = now
        return

    def construct_WU_URL(self, now):
        """ Construct a multi-feature WU API URL

            WU API allows multiple feature requests to be combined into a single
            http request (thus cutting down on API calls. Look at each of our WU
            queries then construct and return a WU API URL string that will
            request all features that are due. If no features are due then
            return None.
        """

        _feature_string = ''
        for _wuq in self.WU_queries:
            # if we haven't made this feature request previously or if its been
            # too long since the last call then make the call
            if (_wuq['last'] is None) or ((now + 1 - _wuq['interval']) >= _wuq['last']):
                # we need to request this feature so add the feature code to our
                # feature string
                if len(_feature_string) > 0:
                    _feature_string += '/' + _wuq['name']
                else:
                    _feature_string += _wuq['name']
                _wuq['last'] = now
        if len(_feature_string) > 0:
            # we have a feature we need so construct the URL
            url = '%s/%s/%s/lang:DL/pws:1/q/%s.json' % (self.default_url, self.api_key,
                                                _feature_string, self.location)
            return (url, _feature_string)
        return (None, None)

    def get_WU_response(self, WU_URL, max_WU_tries):
        """ Make a WU API call and return the raw response.
        """

        # we will attempt the call max_WU_tries times
        for count in range(max_WU_tries):
            # attempt the call
            try:
                w = urllib2.urlopen(WU_URL)
                _WUresponse = w.read()
                w.close()
                return _WUresponse
            except Exception, e:
                loginf("WdSuppArchive:", "Failed to get Weather Underground API response on attempt %d: %s" % (count + 1, e))
        else:
            loginf("WdSuppArchive:", "Failed to get Weather Underground API response")
        return None

    def parse_WU_response(self, response, units):
        """ Parse a potentially multi-feature API response and construct a data
            dict with the required fields.
        """

        # Create a holder dict for the data we will gather
        _data = {}
        # Do some pre-processing and error checking
        if response is not None:
            # We have a response
            # Deserialise our JSON response
            _json_response = json.loads(response)
            # Check for recognised format
            if not 'response' in _json_response:
                loginf("WdSuppArchive:", "Unknown format in Weather Underground API response")
                return _data
            # Get the WU 'response' field so we can check for errors
            _response = _json_response['response']
            # Check for WU provided error otherwise start pulling in the fields/data we want
            if 'error' in _response:
                loginf("WdSuppArchive:", "Error in Weather Underground API response")
                return _data
            # Pull out our individual 'feature' responses, this way in the
            # future we can populate our results even if we did not get a
            # 'feature' response that time round
            for _wuq in self.WU_queries:
                if _wuq['json_title'] in _json_response:
                    _wuq['response'] = _json_response[_wuq['json_title']]
        # Step through each of possible queries and parse as required
        for _wuq in self.WU_queries:
            # Forecast data
            if _wuq['name'] == 'forecast' and _wuq['response'] is not None:
                # Look up Saratoga icon number given WU icon name
                _data['forecastIcon'] = icon_dict[_wuq['response']['txt_forecast']['forecastday'][0]['icon']]
                _data['forecastText'] = _wuq['response']['txt_forecast']['forecastday'][0]['fcttext']
                _data['forecastTextMetric'] = _wuq['response']['txt_forecast']['forecastday'][0]['fcttext_metric']
                _data['pop'] = _wuq['response']['txt_forecast']['forecastday'][0]['pop']
            # Conditions data
            elif _wuq['name'] == 'conditions' and _wuq['response'] is not None:
                # WU does not seem to provide day/night icon name in their 'conditions' response so we
                # need to do. Just need to add 'nt_' to front of name before looking up in out Saratoga
                # icons dictionary
                #if self.night:
                #    _data['currentIcon'] = icon_dict['nt_' + _wuq['response']['icon']]
                #else:
                _data['currentIcon'] = icon_dict[_wuq['response']['icon']]
                _data['currentText'] = _wuq['response']['weather']
                _data['visibility_km'] = _wuq['response']['visibility_km']
            # Almanac data
            elif _wuq['name'] == 'almanac' and _wuq['response'] is not None:
                if units is weewx.US:
                    _data['tempRecordHigh'] = _wuq['response']['temp_high']['record']['F']
                    _data['tempNormalHigh'] = _wuq['response']['temp_high']['normal']['F']
                    _data['tempRecordLow'] = _wuq['response']['temp_low']['record']['F']
                    _data['tempNormalLow'] = _wuq['response']['temp_low']['normal']['F']
                else:
                    _data['tempRecordHigh'] = _wuq['response']['temp_high']['record']['C']
                    _data['tempNormalHigh'] = _wuq['response']['temp_high']['normal']['C']
                    _data['tempRecordLow'] = _wuq['response']['temp_low']['record']['C']
                    _data['tempNormalLow'] = _wuq['response']['temp_low']['normal']['C']
                _data['tempRecordHighYear'] = _wuq['response']['temp_high']['recordyear']
                _data['tempRecordLowYear'] = _wuq['response']['temp_low']['recordyear']
        return _data

    def process_loop(self):
        """ Process latest loop data and populate fields as appropriate.

            Adds following fields (if available) to data dictionary:
                - forecast icon (Vantage only)
                - forecast rule (Vantage only)(Note returns full text forecast)
                - stormRain (Vantage only)
                - stormStart (Vantage only)
                - current theoretical max solar radiation
        """

        # holder dictionary for our gathered data
        _data = {}
        # Vantage forecast icon
        if 'forecastIcon' in self.loop_packet:
            _data['vantageForecastIcon'] = self.loop_packet['forecastIcon']
        # Vantage forecast rule
        if 'forecastRule' in self.loop_packet:
            try:
                _data['vantageForecastRule'] = davis_fr_dict[self.loop_packet['forecastRule']]
                _data['vantageForecastNumber'] = self.loop_packet['forecastRule']
            except:
                _data['vantageForecastRule'] = ""
                logdbg2("WdSuppArchive:", 'Could not decode Vantage forecast code')
        # Vantage stormRain
        if 'stormRain' in self.loop_packet:
            _data['stormRain'] = self.loop_packet['stormRain']
        # Vantage stormStart
        if 'stormStart' in self.loop_packet:
            _data['stormStart'] = self.loop_packet['stormStart']
        # theoretical solar radiation value
        _data['maxSolarRad'] = self.loop_packet['maxSolarRad']
        return _data

    @staticmethod
    def save_record(dbm, _data_record, max_tries = 3, retry_wait = 2):
        """ Save a data record to our database.
        """

        for count in range(max_tries):
            try:
                # save our data to the database
                dbm.addRecord(_data_record)
                break
            except Exception, e:
                logerr("WdSuppArchive:", 'save failed (attempt %d of %d): %s' %
                       ((count + 1), max_tries, e))
                logerr("WdSuppArchive:", 'waiting %d seconds before retry' % (retry_wait, ))
                time.sleep(retry_wait)
        else:
            raise Exception('save failed after %d attempts' % max_tries)

    @staticmethod
    def prune(dbm, ts, max_tries = 3, retry_wait = 2):
        """ Remove records older than ts from the database.
        """

        sql = "delete from %s where dateTime < %d" % (dbm.table_name, ts)
        for count in range(max_tries):
            try:
                dbm.getSql(sql)
                break
            except Exception, e:
                logerr("WdSuppArchive:", 'prune failed (attempt %d of %d): %s' % ((count+1), max_tries, e))
                logerr("WdSuppArchive:", 'waiting %d seconds before retry' % (retry_wait, ))
                time.sleep(retry_wait)
        else:
            raise Exception('prune failed after %d attemps' % max_tries)
        return

    @staticmethod
    def vacuum_database(dbm):
        """ Vacuum our database to save space.
        """

        # SQLite databases need a little help to prevent them from continually
        # growing in size even though we prune records from the database.
        # Vacuum will only work on SQLite databases.  It will compact the
        # database file. It should be OK to run this on a MySQL database - it
        # will silently fail.

        # remove timing code once we get a handle on how long this takes
        # Get time now as a ts
        t1 = time.time()
        try:
            dbm.getSql('vacuum')
        except Exception, e:
            logerr("WdSuppArchive:", 'Vacuuming database % failed: %s' % (dbm.database_name, e))

        t2 = time.time()
        logdbg("WdSuppArchive:", "vacuum_database executed in %0.9f seconds" % (t2-t1))

    def setup_database(self, config_dict):
        """ Setup the database table we will be using.
        """

        # This will create the database and/or table if either doesn't exist,
        # then return an opened instance of the database manager.
        dbmanager = self.engine.db_binder.get_database(self.binding,
                                                       initialize = True)
        loginf("WdSuppArchive:", "Using binding '%s' to database '%s'" %
                                        (self.binding, dbmanager.database_name))

    def new_loop_packet(self, event):
        """ Save Davis Console forecast data that arrives in loop packets so
            we can save it to archive later.

            The Davis Console forecast data is published in each loop packet.
            There is little benefit in saving this data to database each loop
            period as the data is slow changing so we will stash the data and
            save to database each archive period along with our WU sourced data.
        """

        # update our stashed loop packet data
        # wrap in a try..except just in case
        try:
            if 'forecastIcon' in event.packet:
                self.loop_packet['forecastIcon'] = event.packet['forecastIcon']
            else:
                self.loop_packet['forecastIcon'] = None
            if 'forecastRule' in event.packet:
                self.loop_packet['forecastRule'] = event.packet['forecastRule']
            else:
                self.loop_packet['forecastRule'] = None
            if 'stormRain' in event.packet:
                self.loop_packet['stormRain'] = event.packet['stormRain']
            else:
                self.loop_packet['stormRain'] = None
            if 'stormStart' in event.packet:
                self.loop_packet['stormStart'] = event.packet['stormStart']
            else:
                self.loop_packet['stormStart'] = None
            if 'maxSolarRad' in event.packet:
                self.loop_packet['maxSolarRad'] = event.packet['maxSolarRad']
            else:
                self.loop_packet['maxSolarRad'] = None
        except:
            loginf("WdSuppArchive:", "new_loop_packet: Loop packet data error. Cannot decode packet: %s" % (e, ))

    def shutDown(self):
        pass

#===============================================================================
#                                 Utilities
#===============================================================================

def toint(label, value_tbc, default_value):
    """ Convert value_tbc to an integer whilst handling None.

        If value_tbc cannot be converted to an integer default_value is
        returned.

        Input:
            label:         String with the name of the parameter being set
            value_tbc:     The value to be converted to an integer
            default_value: The value to be returned if value cannot be
                           converted to an integer
    """

    if isinstance(value_tbc, str) and value_tbc.lower() == 'none':
        value_tbc = None
    if value_tbc is not None:
        try:
            value_tbc = int(value_tbc)
        except Exception, e:
            logerr("weewxwd3:toint:", "bad value '%s' for %s" % (value_tbc, label))
            value_tbc = default_value
    return value_tbc

def calc_daynighttemps(temp, dt):
    """ 'Calculate' value for outTempDay and outTempNight.

        outTempDay and outTempNight are used to determine warmest night
        and coldest day stats. This is done by using two derived
        observations; outTempDay and outTempNight. These observations
        are defined as follows:

        outTempDay:   equals outTemp if time of day is > 06:00 and <= 18:00
                      otherwise it is None
        outTempNight: equals outTemp if time of day is > 18:00 or <= 06:00
                      otherwise it is None

        By adding these derived obs to the schema and loop packet the daily
        summaries for these obs are populated and aggregate stats can be
        accessed as per normal (eg $month.outTempDay.minmax to give the
        coldest max daytime temp in the month). Note that any aggregates that
        rely on the number of records (eg avg) will be meaningless due to
        the way outTempxxxx is calculated.
    """

    if temp is not None:
        # check if record covers daytime (6AM to 6PM) and if so add 'temp' to 'outTempDay'
        # remember record timestamped 6AM belongs in the night time
        if datetime.fromtimestamp(dt - 1).hour < 6 or datetime.fromtimestamp(dt - 1).hour > 17:
            # ie the data packet is from before 6am or after 6pm
            return (None, temp)
        else:
            # ie the data packet is from after 6am and before or including 6pm
            return (temp, None)
    else:
        return (None, None)

def check_enable(cfg_dict, service, *args):

    try:
        wdsupp_dict = accumulateLeaves(cfg_dict[service], max_level = 1)
    except KeyError:
        logdbg2("weewxwd3:check_enable:", "%s: No config info. Skipped." % service)
        return None

    # Check to see whether all the needed options exist, and none of them have
    # been set to 'replace_me':
    try:
        for option in args:
            if wdsupp_dict[option] == 'replace_me':
                raise KeyError(option)
    except KeyError, e:
        logdbg2("weewxwd3:check_enable:", "%s: Missing option %s" % (service, e))
        return None

    return wdsupp_dict

Reply via email to