#!/usr/bin/python
# $Id: config_fousb.py 481 2013-02-14 19:22:56Z mwall $
#
# Copyright 2012 Matthew Wall
#
# 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 3 of the License, or 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 http://www.gnu.org/licenses/

"""Command line utility for configuring Fine Offset weather stations

Many thanks to Jim Easterbrook, author of pywws upon which this is based.

The station model, version, and id are supposed to be reported by these
instruments, but so far (04jan2013) my testing shows bogus values for these
fields.  The documentation indicates that the clock should be settable, but
so far (14feb2013) my testing shows this is not the case.

If you have a Fine Offset station and use this utility, it would be helpful
to know:

1) the model, version, and id

2) the stations model as indicated on the packaging, for example
   'Ambient WS-2080', 'National Geographic 265NE, or 'Watson W8681'

Output from a 308x-series station would be particularly helpful.
"""

import configobj
import datetime
import optparse
import sys
import syslog
import time

import weewx.fousb
import weewx.units
import weeutil.weeutil

description = """Configuration utility for Fine Offset weather stations."""

usage = """%prog config_file [options]"""

epilog = """Mutating actions will request confirmation before proceeding.
Attempts to set the station clock will probably fail."""

def main():
    syslog.openlog('wee_config_fousb', syslog.LOG_PID|syslog.LOG_CONS)

    # Create a command line parser:
    parser = optparse.OptionParser(description=description, usage=usage, epilog=epilog)
    
    # Add the various options:
    parser.add_option("--info", dest="info", action="store_true",
                      help="display weather station configuration")
    parser.add_option("--check-pressures", dest="chkpres", action="store_true",
                      help="query station for pressure sensor data")
    parser.add_option("--check-units", dest="chkunits", action="store_true",
                      help="compare raw and converted LOOP packets")
    parser.add_option("--check-usb", dest="chkusb", action="store_true",
                      help="test the quality of the USB connection")
    parser.add_option("--history", dest="history", type=int, metavar="N",
                      help="display the latest N records")
    parser.add_option("--format", dest="format", type=str, metavar="FORMAT",
                      help="format for output, one of raw, table, or dict")
    parser.add_option("--set-clock", dest="clock", action="store_true",
                      help="set station clock to computer time")
    parser.add_option("--set-pressure", dest="pressure",type=float,metavar="P",
                      help="set relative pressure to P hPa (mbar)")
    parser.add_option("--set-interval", dest="interval", type=int, metavar="N",
                      help="set logging interval to N minutes")
    parser.add_option("--clear-memory", dest="clear", action="store_true",
                      help="clear station memory")
    parser.add_option("-y", dest="noprompt", action="store_true",
                      help="answer yes to every prompt")
    parser.add_option("--debug", dest="debug", action="store_true",
                      help="display diagnostic information while running")

    # Now we are ready to parse the command line:
    (options, args) = parser.parse_args()
    if not args:
        parser.error("No configuration file specified")

    cfgfile = args[0]
    debug = options.debug or weewx.debug

    # Try to open up the configuration file. Declare an error if unable to.
    try :
        config_dict = configobj.ConfigObj(cfgfile, file_error=True)
    except IOError:
        print "Unable to open configuration file %s" % cfgfile
        exit(1)
    except configobj.ConfigObjError:
        print "Error wile parsing configuration file %s" % cfgfile
        exit(1)

    if debug:
        print "Using configuration file %s" % cfgfile

    # station needs altitude to do calculations
    altitude_t = weeutil.weeutil.option_as_list(config_dict['Station'].get('altitude', (None, None)))
    # form a units value-tuple
    altitude_vt = (float(altitude_t[0]), altitude_t[1], "group_altitude")
    # convert to meters
    altitude_m = weewx.units.convert(altitude_vt, 'meter')[0]
    
    if debug:
        print 'altitude from config: %s' % altitude_t
        print 'altitude in meters: %s' % altitude_m

    station = weewx.fousb.FineOffsetUSB(altitude=altitude_m,
                                        **config_dict['FineOffsetUSB'])

    if options.noprompt:
        prompt = False
    else:
        prompt = True

    if options.history is not None:
        showhistory(station, options.history, options.format)
    elif options.chkunits:
        checkunits(station, config_dict['StdConvert']['target_unit'])
    elif options.chkpres:
        checkpressures(station, config_dict['StdConvert']['target_unit'],
                       altitude_t[0], altitude_t[1])
    elif options.chkusb:
        checkusb(station)
    elif options.clock:
        setclock(station, prompt)
    elif options.pressure is not None:
        setpressure(station, options.pressure, prompt)
    elif options.interval is not None:
        setinterval(station, options.interval, prompt)
    elif options.clear:
        clearhistory(station)
    elif options.info or len(args) == 1:
        info(station)

def info(station):
    """Query the station then display the settings."""

    print "Querying the station..."
    val = getvalues(station, '', weewx.fousb.fixed_format)
    station.closePort()

    print 'Fine Offset station settings:'
    print '%s: %s' % ('local time'.rjust(30),
                      time.strftime('%Y.%m.%d %H:%M:%S %Z', time.localtime()))
    print '%s: %s' % ('polling mode'.rjust(30), station.polling_mode)

    slist = {'values':[], 'minmax_values':[],
             'settings':[], 'display_settings':[], 'alarm_settings':[]}
    for x in sorted(val.keys()):
        if type(val[x]) is dict:
            for y in val[x].keys():
                label = x + '.' + y
                s = fmtparam(label, val[x][y])
                slist = stash(slist, s)
        else:
            s = fmtparam(x, val[x])
            slist = stash(slist, s)
    for k in ('values','minmax_values','settings','display_settings','alarm_settings'):
        print ''
        for s in slist[k]:
            print s

def checkpressures(station, target_unit_name, altitude_value, altitude_units):
    """Query the station then display pressure-related sensor readings."""

    target_unit = weewx.units.unit_constants[target_unit_name.upper()]
    converter = weewx.units.StdUnitConverters[target_unit]

    print "Querying the station..."
    for packet in station.genLoopPackets():
        sp = packet['pressure']
        ap = station.get_fixed_block(['abs_pressure'])
        rp = station.get_fixed_block(['rel_pressure'])
        ap1 = weewx.wxformulas.altimeter_pressure_Metric(sp, station.altitude)
        ap2 = weewx.fousb.sp2ap(sp, station.altitude)
        bp2 = weewx.fousb.sp2bp(sp, station.altitude, packet['outTemp'])
        print 'altitude: %s %s' % (altitude_value, altitude_units)
        print 'station pressure (sensor): %s' % sp
        print 'absolute pressure (fixed_block): %s' % ap
        print 'relative pressure (fixed_block): %s' % rp
        print 'altimeter pressure (davis algorithm): %s' % ap1
        print 'altimeter pressure (noaa algorithm): %s' % ap2
        print 'barometer pressure (wview algorithm): %s' % bp2
        print 'raw LOOP packet:'
        print packet
        cpkt = converter.convertDict(packet)
        print 'converted LOOP packet:'
        print cpkt
        break
    station.closePort()

def checkunits(station, target_unit_name):
    """Query the station then display raw and converted sensor readings."""

    target_unit = weewx.units.unit_constants[target_unit_name.upper()]
    converter = weewx.units.StdUnitConverters[target_unit]

    print "Querying the station..."
    for packet in station.genLoopPackets():
        print 'target_unit: %s' % target_unit_name
        print 'raw LOOP packet:'
        print packet
        print 'converted LOOP packet:'
        print converter.convertDict(packet)
        break
    station.closePort()

def checkusb(station):
    """Run diagnostics on the USB connection."""
    print "This will read from the station console repeatedly to see if there"
    print "are errors in the USB communications.  Leave this running for an"
    print "hour or two to see if any bad reads are encountered.  Bad reads"
    print "will be reported in the system log.  A few bad reads per hour is"
    print "usually acceptable."
    ptr = weewx.fousb.data_start
    total_count = 0
    bad_count = 0
    while True:
        if total_count % 1000 == 0:
            active = station.current_pos()
        while True:
            ptr += 0x20
            if ptr >= 0x10000:
                ptr = weewx.fousb.data_start
            if active < ptr - 0x10 or active >= ptr + 0x20:
                break
        result_1 = station._read_block(ptr, retry=False)
        result_2 = station._read_block(ptr, retry=False)
        if result_1 != result_2:
            syslog.syslog(syslog.LOG_INFO, 'read_block changing %06x' % ptr)
            syslog.syslog(syslog.LOG_INFO, '  %s' % str(result_1))
            syslog.syslog(syslog.LOG_INFO, '  %s' % str(result_2))
            bad_count += 1
        total_count += 1
        print "\rbad/total: %d/%d " % (bad_count, total_count),
        sys.stdout.flush()

def showhistory(station, count, format):
    """Query the station for the latest count records."""

    if format is None:
        format = 'table'
    elif format.lower() != 'raw' and format.lower() != 'table' and format.lower() != 'dict':
        print "Unknown format '%s'" % format
        exit(1)

    print "Querying the station..."
    fixed_block = station.get_fixed_block()
    ptr = fixed_block['current_pos']
    date = datetime.datetime.now().replace(second=0, microsecond=0)
    for i in range(count):
        data = station.get_data(ptr)
        if format.lower() == 'raw':
            raw_dump(ptr, station.get_raw_data(ptr))
        else:
            if format.lower() == 'table':
                table_dump(date, data, i==0)
            else:
                print date, data
            date = date - datetime.timedelta(minutes=data['delay'])
        ptr = station.dec_ptr(ptr)

def clearhistory(station):
    ans = None
    while ans not in ['y', 'n']:
        v = station.get_fixed_block(['data_count'], True)
        print "Records in memory:", v
        ans = raw_input("Clear console memory (y/n)? ")
        if ans == 'y' :
            ptr = weewx.fousb.fixed_format['data_count'][0]
            data = []
            data.append((ptr,   1))
            data.append((ptr+1, 0))
            station.write_data(data)
            v = station.get_fixed_block(['data_count'], True)
            print "Records in memory:", v
        elif ans == 'n':
            print "Clear memory cancelled."

def setpressure(station, pressure, prompt):
    v = station.get_fixed_block(['rel_pressure'], True)
    ans = None
    while ans not in ['y', 'n']:
        print "Relative pressure is", v
        if prompt:
            ans = raw_input("Set pressure to %f hPa (y/n)? " % pressure)
        else:
            print "Setting pressure to %f hPa" % pressure
            ans = 'y'
        if ans == 'y' :
            p = pressure * 10.0
            ptr = weewx.fousb.fixed_format['rel_pressure'][0]
            data = []
            data.append((ptr,   p % 256))
            data.append((ptr+1, p // 256))
            station.write_data(data)
            v = station.get_fixed_block(['rel_pressure'], True)
            print "Relative pressure is now", v
        elif ans == 'n':
            print "Set pressure cancelled."

def setinterval(station, read_period, prompt):
    v = station.get_fixed_block(['read_period'], True)
    ans = None
    while ans not in ['y', 'n']:
        print "Interval is", v
        if prompt:
            ans = raw_input("Set interval to %d minutes (y/n)? " % read_period)
        else:
            print "Setting interval to %d minutes" % read_period
            ans = 'y'
        if ans == 'y' :
            data = []
            data.append((weewx.fousb.fixed_format['read_period'][0],
                         read_period))
            station.write_data(data)
            v = station.get_fixed_block(['read_period'], True)
            print "Interval is now", v
        elif ans == 'n':
            print "Set interval cancelled."

# fine offset documentation indicates that this should work, but so far it
# has not worked on any ambient weather WS2080 or WS1090 station i have tried.
# it looks like the station clock is set, but at some point the fixed block
# reverts to the previous clock value.  also unclear is the behavior when the
# station attempts to sync with radio clock signal from sensor.
# -- mwall 14feb2013
def setclock(station, prompt):
    ans = None
    while ans not in ['y', 'n']:
        v = station.get_fixed_block(['date_time'], True)
        print "Station clock is", v
        ts = datetime.datetime.now()
        if prompt:
            ans = raw_input("Set station clock to %s (y/n)? " % ts)
        else:
            print "Setting station clock to %s" % ts
            ans = 'y'
        if ans == 'y' :
            now = datetime.datetime.now()
            if now.second >= 55:
                time.sleep(10)
                now = datetime.datetime.now()
            now += datetime.timedelta(minutes=1)
            ptr = weewx.fousb.fixed_format['date_time'][0]
            data = []
            data.append((ptr,   bcd_encode(now.year - 2000)))
            data.append((ptr+1, bcd_encode(now.month)))
            data.append((ptr+2, bcd_encode(now.day)))
            data.append((ptr+3, bcd_encode(now.hour)))
            data.append((ptr+4, bcd_encode(now.minute)))
            time.sleep(59 - now.second)
            station.write_data(data)
            v = station.get_fixed_block(['date_time'], True)
            print "Station clock is now", v
        elif ans == 'n':
            print "Set clock cancelled."

def stash(slist, s):
    if s.find('settings') != -1:
        slist['settings'].append(s)
    elif s.find('display') != -1:
        slist['display_settings'].append(s)
    elif s.find('alarm') != -1:
        slist['alarm_settings'].append(s)
    elif s.find('min.') != -1 or s.find('max.') != -1:
        slist['minmax_values'].append(s)
    else:
        slist['values'].append(s)
    return slist

def fmtparam(label, value):
    fmt = '%s'
    if label in weewx.fousb.datum_display_formats.keys():
        fmt = weewx.fousb.datum_display_formats[label]
    fmt = '%s: ' + fmt
    return fmt % (label.rjust(30), value)

def getvalues(station, name, value):
    values = {}
    if type(value) is tuple:
        values[name] = station.get_fixed_block(name.split('.'))
    elif type(value) is dict:
        for x in value.keys():
            n = x
            if len(name) > 0:
                n = name + '.' + x
            values.update(getvalues(station, n, value[x]))
    return values

def raw_dump(pos, data):
    print "%04x" % pos,
    for item in data:
        print "%02x" % item,
    print

def table_dump(date, data, showlabels=False):
    if showlabels:
        print '# date time',
        for key in data.keys():
            print key,
        print
    print date,
    for key in data.keys():
        print data[key],
    print

def bcd_encode(value):
    hi = value // 10
    lo = value % 10
    return (hi * 16) + lo

if __name__=="__main__" :
    main()
