#!/usr/bin/env python
#
# Copyright 2014 Matthew Wall
# See the file LICENSE.txt for your rights.

"""Driver for CC3000 data logger

http://www.rainwise.com/products/attachments/6832/20110518125531.pdf

There are a few variants:

CC-3000_ - __
       |   |
       |   41 = 418 MHz
       |   42 = 433 MHz
       |   __ = 2.4 GHz (LR compatible)
       R = serial (RS232, RS485)
       _ = USB 2.0

The CC3000 communicates using FTDI USB serial bridge.  The CC3000R has both
RS-232 and RS-485 serial ports, only one of which may be used at a time.
A long range (LR) version transmits up to 2 km using 2.4GHz.

The RS232 communicates using 115200 N-8-1

The instrument cluster contains a DIP switch controls with value 0-3 and a
default of 0.  This setting prevents interference when there are multiple
weather stations within radio range.

The CC3000 includes a temperature sensor - that is the source of inTemp.  The
manual indicates that the CC3000 should run for 3 or 4 hours before applying
any calibration to offset the heat generated by CC3000 electronics.

The CC3000 uses 4 AA batteries to maintain its clock.  Use only rechargeable
NiMH batteries.

The logger contains 2MB of memory, with a capacity of 49834 records (over 11
months of data at a 10 minute logging interval).  The exact capacity depends
on the sensors; the basic sensor record is 42 bytes.

The logger does not delete old records when it fills up; once the logger is
full, new data are lost.  So the driver must periodically clear the logger
memory.

This driver does not support hardware record_generation.  It does support
catchup on startup.

If you request many history records then interrupt the receive, the logger will
continue to send history records until it sends all that were requested.  As a
result, any queries made while the logger is still sending will fail.

The rainwise rain bucket measures 0.01 inches per tip.  The logger firmware
automatically converts the bucket tip count to the measure of rain in ENGLISH
or METRIC units.

Logger uses the following units:
               ENGLISH  METRIC
  wind         mph      m/s
  rain         inch     mm
  pressure     inHg     mbar
  temperature  F        C

This driver was tested with:
  Rainwise CC-3000 Version: 1.3 Build 006 Sep 04 2013
  Rainwise CC-3000 Version: 1.3 Build 016 Aug 21 2014
"""

# FIXME: confirm that rain field in archive records is a total, not a delta
# FIXME: figure out whether logger retains non-fixed interval data
# FIXME: clear logger memory after successful read
# FIXME: periodically clear the logger memory while running

from __future__ import with_statement
import datetime
import serial
import string
import syslog
import time

from weeutil.weeutil import to_bool
import weewx.drivers

DRIVER_NAME = 'CC3000'
DRIVER_VERSION = '0.13'

def loader(config_dict, engine):
    return CC3000Driver(**config_dict[DRIVER_NAME])

def configurator_loader(config_dict):
    return CC3000Configurator()

def confeditor_loader():
    return CC3000ConfEditor()

DEBUG_SERIAL = 0
DEBUG_CHECKSUM = 0
DEBUG_OPENCLOSE = 0

def logmsg(level, msg):
    syslog.syslog(level, 'cc3000: %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 ChecksumError(weewx.WeeWxIOError):
    def __init__(self, msg):
        weewx.WeeWxIOError.__init__(self, msg)

class ChecksumMismatch(ChecksumError):
    def __init__(self, a, b, buf=None):
        msg = "Checksum mismatch: 0x%04x != 0x%04x" % (a, b)
        if buf is not None:
            msg = "%s (%s)" % (msg, _fmt(buf))
        ChecksumError.__init__(self, msg)

class BadCRC(ChecksumError):
    def __init__(self, a, b, buf=None):
        msg = "Bad CRC: 0x%04x != '%s'" % (a, b)
        if buf is not None:
            msg = "%s (%s)" % (msg, _fmt(buf))
        ChecksumError.__init__(self, msg)


class CC3000Configurator(weewx.drivers.AbstractConfigurator):
    def add_options(self, parser):
        super(CC3000Configurator, self).add_options(parser)
        parser.add_option("--info", dest="info", action="store_true",
                          help="display weather station configuration")
        parser.add_option("--current", dest="current", action="store_true",
                          help="display current weather readings")
        parser.add_option("--history", dest="nrecords", type=int, metavar="N",
                          help="display N records (0 for all records)")
        parser.add_option("--history-since", dest="nminutes", metavar="N",
                          type=int, help="display records since N minutes ago")
        parser.add_option("--clear-memory", dest="clear", action="store_true",
                          help="clear station memory")
        parser.add_option("--reset-rain", dest="reset", action="store_true",
                          help="reset the rain counter")
        parser.add_option("--get-clock", dest="getclock", action="store_true",
                          help="display station clock")
        parser.add_option("--set-clock", dest="setclock", action="store_true",
                          help="set station clock to computer time")
        parser.add_option("--get-interval", dest="getint", action="store_true",
                          help="display logger archive interval, in minutes")
        parser.add_option("--set-interval", dest="interval", metavar="N",
                          type=int,
                          help="set logging interval to N minutes (0-60)")
        parser.add_option("--get-units", dest="getunits", action="store_true",
                          help="show units of logger")
        parser.add_option("--set-units", dest="units", metavar="UNITS",
                          help="set units to METRIC or ENGLISH")
        parser.add_option('--get-dst', dest='getdst', action='store_true',
                          help='display daylight savings settings')
        parser.add_option('--set-dst', dest='dst',
                          metavar='mm/dd HH:MM,mm/dd HH:MM,[MM]M',
                          help='set daylight savings start, end, and amount')
        parser.add_option("--get-channel", dest="getch", action="store_true",
                          help="display the station channel")
        parser.add_option("--set-channel", dest="ch", metavar="CHANNEL",
                          type=int,
                          help="set the station channel")

    def do_options(self, options, parser, config_dict, prompt):
        self.driver = CC3000Driver(**config_dict[DRIVER_NAME])
        if options.current:
            print self.driver.get_current()
        elif options.nrecords is not None:
            for r in self.driver.station.gen_records(nrecords):
                print r
        elif options.clear:
            self.clear_memory(prompt)
        elif options.reset:
            self.reset_rain(prompt)
        elif options.getclock:
            print self.driver.station.get_time()
        elif options.setclock:
            self.set_clock(prompt)
        elif options.getdst:
            print self.driver.station.get_dst()
        elif options.dst is not None:
            self.set_dst(options.setdst, prompt)
        elif options.getint:
            print self.driver.station.get_interval() * 60
        elif options.interval is not None:
            self.set_interval(options.interval / 60, prompt)
        elif options.getunits:
            print self.driver.station.get_units()
        elif options.units is not None:
            self.set_units(options.units, prompt)
        elif options.getch:
            print self.driver.station.get_channel()
        elif options.ch is not None:
            self.set_channel(options.ch, prompt)
        else:
            print "firmware:", self.driver.station.get_version()
            print "time:", self.driver.station.get_time()
            print "dst:", self.driver.station.get_dst()
            print "units:", self.driver.station.get_units()
            print "memory:", self.driver.station.get_memory_status()
            print "interval:", self.driver.station.get_interval() * 60
            print "channel:", self.driver.station.get_channel()
            print "charger:", self.driver.station.get_charger()
            print "baro:", self.driver.station.get_baro()
        self.driver.closePort()

    def clear_memory(self, prompt):
        ans = None
        while ans not in ['y', 'n']:
            print self.driver.station.get_memory_status()
            if prompt:
                ans = raw_input("Clear console memory (y/n)? ")
            else:
                print 'Clearing console memory'
                ans = 'y'
            if ans == 'y':
                self.driver.station.clear_memory()
                print self.driver.station.get_memory_status()
            elif ans == 'n':
                print "Clear memory cancelled."

    def reset_rain(self, prompt):
        ans = None
        while ans not in ['y', 'n']:
            print self.driver.station.get_rain()
            if prompt:
                ans = raw_input("Reset rain counter (y/n)? ")
            else:
                print 'Resetting rain counter'
                ans = 'y'
            if ans == 'y':
                self.driver.station.reset_rain()
                print self.driver.station.get_rain()
            elif ans == 'n':
                print "Reset rain cancelled."

    def set_interval(self, interval, prompt):
        if interval < 0 or 60 < interval:
            raise ValueError("Logger interval must be 0-60 minutes")
        ans = None
        while ans not in ['y', 'n']:
            print "Interval is", self.driver.station.get_interval()
            if prompt:
                ans = raw_input("Set interval to %d minutes (y/n)? " % interval)
            else:
                print "Setting interval to %d minutes" % interval
                ans = 'y'
            if ans == 'y':
                self.driver.station.set_interval(interval)
                print "Interval is now", self.driver.station.get_interval()
            elif ans == 'n':
                print "Set interval cancelled."

    def set_clock(self, prompt):
        ans = None
        while ans not in ['y', 'n']:
            print "Station clock is", self.driver.station.get_time()
            now = datetime.datetime.now()
            if prompt:
                ans = raw_input("Set station clock to %s (y/n)? " % now)
            else:
                print "Setting station clock to %s" % now
                ans = 'y'
            if ans == 'y':
                self.driver.station.set_time()
                print "Station clock is now", self.driver.station.get_time()
            elif ans == 'n':
                print "Set clock cancelled."

    def set_units(self, units, prompt):
        if units.lower() not in ['metric', 'english']:
            raise ValueError("Units must be METRIC or ENGLISH")
        ans = None
        while ans not in ['y', 'n']:
            print "Station units is", self.driver.station.get_units()
            if prompt:
                ans = raw_input("Set station units to %s (y/n)? " % units)
            else:
                print "Setting station units to %s" % units
                ans = 'y'
            if ans == 'y':
                self.driver.station.set_units(units)
                print "Station units is now", self.driver.station.get_units()
            elif ans == 'n':
                print "Set units cancelled."

    def set_dst(self, dst, prompt):
        if dst != '0' and len(dst.split(',')) != 3:
            raise ValueError("DST must be 0 (disabled) or start, stop, amount "
                             "with the format mm/dd HH:MM, mm/dd HH:MM, [MM]M")
        ans = None
        while ans not in ['y', 'n']:
            print "Station DST is", self.driver.station.get_dst()
            if prompt:
                ans = raw_input("Set DST to %s (y/n)? " % dst)
            else:
                print "Setting station DST to %s" % dst
                ans = 'y'
            if ans == 'y':
                self.driver.station.set_dst(dst)
                print "Station DST is now", self.driver.station.get_dst()
            elif ans == 'n':
                print "Set DST cancelled."

    def set_channel(self, ch, prompt):
        if ch not in [0, 1, 2, 3]:
            raise ValueError("Channel must be one of 0, 1, 2, or 3")
        ans = None
        while ans not in ['y', 'n']:
            print "Station channel is", self.driver.station.get_channel()
            if prompt:
                ans = raw_input("Set channel to %s (y/n)? " % ch)
            else:
                print "Setting station channel to %s" % ch
                ans = 'y'
            if ans == 'y':
                self.driver.station.set_channel(ch)
                print "Station channel is now", self.driver.station.get_ch()
            elif ans == 'n':
                print "Set channel cancelled."


class CC3000Driver(weewx.drivers.AbstractDevice):
    """weewx driver that communicates with a RainWise CC3000 data logger."""

    # map rainwise names to database schema names
    DEFAULT_SENSOR_MAP = {
        'dateTime': 'TIMESTAMP',
        'outTemp': 'TEMP OUT',
        'outHumidity': 'HUMIDITY',
        'windDir': 'WIND DIRECTION',
        'windSpeed': 'WIND SPEED',
        'windGust': 'WIND GUST',
        'pressure': 'PRESSURE',
        'inTemp': 'TEMP IN',
        'extraTemp1': 'TEMP 1',
        'extraTemp2': 'TEMP 2',
        'day_rain_total': 'RAIN',
        'consBatteryVoltage': 'STATION BATTERY',
        'bkupBatteryVoltage': 'BATTERY BACKUP',
        'radiation': 'SOLAR RADIATION',
        'UV': 'UV INDEX',
    }

    def __init__(self, **stn_dict):
        loginf('driver version is %s' % DRIVER_VERSION)

        global DEBUG_SERIAL
        DEBUG_SERIAL = int(stn_dict.get('debug_serial', 0))
        global DEBUG_CHECKSUM
        DEBUG_CHECKSUM = int(stn_dict.get('debug_checksum', 0))
        global DEBUG_OPENCLOSE
        DEBUG_OPENCLOSE = int(stn_dict.get('debug_openclose', 0))

        self.model = stn_dict.get('model', 'CC3000')
        port = stn_dict.get('port', CC3000.DEFAULT_PORT)
        loginf('using serial port %s' % port)
        self.polling_interval = float(stn_dict.get('polling_interval', 1))
        loginf('polling interval is %s seconds' % self.polling_interval)
        self.use_station_time = to_bool(stn_dict.get('use_station_time', True))
        loginf('using %s time for loop packets' %
               ('station' if self.use_station_time else 'computer'))
        self.max_tries = int(stn_dict.get('max_tries', 5))
        self.retry_wait = int(stn_dict.get('retry_wait', 60))
        self.sensor_map = stn_dict.get('sensor_map', self.DEFAULT_SENSOR_MAP)
        self.last_rain = None

        self.station = CC3000(port)
        self.station.open()

        # report the station configuration
        settings = self._init_station_with_retries(
            self.station, self.max_tries, self.retry_wait)
        loginf('firmware: %s' % settings['firmware'])
        self.arcint = settings['arcint']
        loginf('archive_interval: %s' % self.arcint)
        self.header = settings['header']
        loginf('header: %s' % self.header)
        self.units = weewx.METRICWX if settings['units'] == 'METRIC' else weewx.US
        loginf('units: %s' % settings['units'])
        loginf('channel: %s' % settings['channel'])
        loginf('charger status: %s' % settings['charger'])

    def genLoopPackets(self):
        cmd_mode = True
        if self.polling_interval == 0:
            self.station.set_auto()
            cmd_mode = False
        ntries = 0
        while ntries < self.max_tries:
            ntries += 1
            try:
                values = self.station.get_current_data(cmd_mode)
                ntries = 0
                logdbg("values: %s" % values)
                packet = self._parse_current(
                    values, self.header, self.sensor_map)
                logdbg("parsed: %s" % packet)
                if packet and 'dateTime' in packet:
                    if not self.use_station_time:
                        packet['dateTime'] = int(time.time() + 0.5)
                    packet['usUnits'] = self.units
                    if 'day_rain_total' in packet:
                        packet['rain'] = self._rain_total_to_delta(
                            packet['day_rain_total'], self.last_rain)
                        self.last_rain = packet['day_rain_total']
                    else:
                        loginf("no rain in loop values: %s" % values)
                    logdbg("packet: %s" % packet)
                    yield packet
                if self.polling_interval:
                    time.sleep(self.polling_interval)
            except (serial.serialutil.SerialException, weewx.WeeWxIOError), e:
                logerr("Failed attempt %d of %d to get data: %s" %
                       (ntries, self.max_tries, e))
                logdbg("Waiting %d seconds before retry" % self.retry_wait)
                time.sleep(self.retry_wait)
        else:
            msg = "Max retries (%d) exceeded" % self.max_tries
            logerr(msg)
            raise weewx.RetriesExceeded(msg)

    def genStartupRecords(self, since_ts):
        """Return archive records from the data logger.  Download all records
        then return the subset since the indicated timestamp.

        Assumptions:
         - the units are consistent for the entire history.
         - the archive interval is constant for entire history.
         - the HDR for archive records is the same as current HDR
        """
        logdbg("genStartupRecords: since_ts=%s" % since_ts)
        nrec = 0
        # figure out how many records we need to download
        if since_ts is not None:
            delta = int(time.time()) - since_ts
            nrec = int(delta / self.arcint)
            logdbg("genStartupRecords: nrec=%d delta=%d" % (nrec, delta))
            if nrec == 0:
                loginf("no need to read logger records")
                return
        else:
            logdbg("genStartupRecords: nrec=%d" % nrec)

        totrec = int(self.station.get_memory_status().split(',')[1].split()[0])
        loginf("download %d of %d records" % (nrec, totrec))
        i = 0
        last_rain = None
        for r in self.station.gen_records(nrec):
            logdbg("values: %s" % r)
            i += 1
            if i % 100 == 0:
                loginf("record %d of %d (total=%d)" % (i, nrec, totrec))
            if r[0] != 'REC':
                logdbg("non REC item: %s" % r[0])
                continue
            pkt = self._parse_historical(r[1:], self.header, self.sensor_map)
            logdbg("parsed: %s" % pkt)
            if 'dateTime' in pkt and pkt['dateTime'] > since_ts:
                pkt['usUnits'] = self.units
                pkt['interval'] = self.arcint
                if 'day_rain_total' in pkt:
                    pkt['rain'] = self._rain_total_to_delta(
                        pkt['day_rain_total'], last_rain)
                    last_rain = pkt['day_rain_total']
                else:
                    loginf("no rain in record: %s" % r)
                logdbg("packet: %s" % pkt)
                yield pkt

    @property
    def hardware_name(self):
        return self.model

    @property
    def archive_interval(self):
        return self.arcint

    def getTime(self):
        try:
            v = self.station.get_time()
            return _to_ts(v)
        except ValueError, e:
            logerr("getTime failed: %s" % e)
        return 0

    def setTime(self):
        self.station.set_time()

    @staticmethod
    def _init_station_with_retries(station, max_tries, retry_wait):
        for cnt in xrange(max_tries):
            try:
                return CC3000Driver._init_station(station)
            except (serial.serialutil.SerialException, weewx.WeeWxIOError), e:
                logerr("Failed attempt %d of %d to initialize station: %s" %
                       (cnt + 1, max_tries, e))
                logdbg("Waiting %d seconds before retry" % retry_wait)
                time.sleep(retry_wait)
        else:
            raise weewx.RetriesExceeded("Max retries (%d) exceeded while initializing station" % max_tries)

    @staticmethod
    def _init_station(station):
        station.flush()
        station.wakeup()
        station.set_echo()
        settings = dict()
        settings['firmware'] = station.get_version()
        settings['arcint'] = station.get_interval() * 60 # arcint is in seconds
        settings['header'] = CC3000Driver._parse_header(station.get_header())
        settings['units'] = station.get_units()
        settings['channel'] = station.get_channel()
        settings['charger'] = station.get_charger()
        return settings

    @staticmethod
    def _rain_total_to_delta(rain_total, last_rain):
        # calculate the rain delta from rain total
        rain_delta = None
        if last_rain is not None:
            if rain_total >= last_rain:
                rain_delta = (rain_total - last_rain)
            else:
                loginf("rain counter rollover detected: new=%s last=%s" %
                       (rain_total, last_rain))
        else:
            loginf("rain skipped for rain_total=%s: no last rain measurement" %
                   rain_total)
        return rain_delta

    @staticmethod
    def _parse_current(values, header, sensor_map):
        return CC3000Driver._parse_values(values, header, sensor_map,
                                          "%Y/%m/%d %H:%M:%S")

    @staticmethod
    def _parse_historical(values, header, sensor_map):
        return CC3000Driver._parse_values(values, header, sensor_map,
                                          "%Y/%m/%d %H:%M")

    @staticmethod
    def _parse_values(values, header, sensor_map, fmt):
        """parse the values and map them into the schema names.  if there is
        a failure for any one value, then the entire record fails."""
        pkt = dict()
        if len(values) != len(header) + 1:
            logdbg("values/header mismatch: %s %s" % (values, header))
            return pkt
        for i, v in enumerate(values):
            if i >= len(header):
                continue
            label = None
            for m in sensor_map:
                if sensor_map[m] == header[i]:
                    label = m
            if label is None:
                continue
            try:
                if header[i] == 'TIMESTAMP':
                    pkt[label] = _to_ts(v, fmt)
                else:
                    pkt[label] = float(v)
            except ValueError, e:
                logerr("parse failed for '%s' '%s': %s (idx=%s values=%s)" %
                       (header[i], v, e, i, values))
                return dict()
        return pkt

    @staticmethod
    def _parse_header(header):
        h = []
        for v in header:
            if v == 'HDR' or v[0:1] == '!':
                continue
            h.append(v.replace('"', ''))
        return h

    def get_current(self):
        data = self.station.get_current_data()
        return self._parse_current(data, self.header, self.sensor_map)


def _to_ts(tstr, fmt="%Y/%m/%d %H:%M:%S"):
    return time.mktime(time.strptime(tstr, fmt))

def _format_bytes(buf):
    return ' '.join(["%0.2X" % ord(c) for c in buf])

def _fmt(buf):
    return filter(lambda x: x in string.printable, buf)

# calculate the crc for a string using CRC-16-CCITT
# http://bytes.com/topic/python/insights/887357-python-check-crc-frame-crc-16-ccitt
def _crc16(data):
    reg = 0x0000
    data += '\x00\x00'
    for byte in data:
        mask = 0x80
        while mask > 0:
            reg <<= 1
            if ord(byte) & mask:
                reg += 1
            mask >>= 1
            if reg > 0xffff:
                reg &= 0xffff
                reg ^= 0x1021
    return reg

def _check_crc(buf):
    idx = buf.find('!')
    if idx < 0:
        return
    a = 0
    b = 0
    cs = ''
    try:
        cs = buf[idx+1:idx+5]
        if DEBUG_CHECKSUM:
            logdbg("found checksum at %d: %s" % (idx, cs))
        a = _crc16(buf[0:idx]) # calculate checksum
        if DEBUG_CHECKSUM:
            logdbg("calculated checksum %x" % a)
        b = int(cs, 16) # checksum provided in data
        if a != b:
            raise ChecksumMismatch(a, b, buf)
    except ValueError, e:
        raise BadCRC(a, cs, buf)

# for some reason we sometimes get null characters randomly mixed in with the
# bytes we receive.  strip them out and let the checksum do the validation of
# the data integrity.
def _strip_unprintables(buf):
    newbuf = ''
    for x in buf:
        if x in string.printable:
            newbuf += x
    return newbuf

class CC3000(object):
    DEFAULT_PORT = '/dev/ttyUSB0'

    def __init__(self, port):
        self.port = port
        self.baudrate = 115200
        self.timeout = 5 # seconds.  clear memory takes 4 seconds.
        self.serial_port = None

    def __enter__(self):
        self.open()
        return self

    def __exit__(self, _, value, traceback):
        self.close()

    def open(self):
        if DEBUG_OPENCLOSE:
            logdbg("open serial port %s" % self.port)
        self.serial_port = serial.Serial(self.port, self.baudrate,
                                         timeout=self.timeout)

    def close(self):
        if self.serial_port is not None:
            if DEBUG_OPENCLOSE:
                logdbg("close serial port %s" % self.port)
            self.serial_port.close()
            self.serial_port = None

    def write(self, data):
        if DEBUG_SERIAL:
            logdbg("write: '%s'" % data)
        n = self.serial_port.write(data)
        if n is not None and n != len(data):
            raise weewx.WeeWxIOError("Write expected %d chars, sent %d" %
                                     (len(data), n))

    def read(self):
        """The station sends CR NL before and after any response.  Some
        responses have a 4-byte CRC checksum at the end, indicated with an
        exclamation.  Not every response has a checksum.
        """
        data = self.serial_port.readline()
        if DEBUG_SERIAL:
            logdbg("read: '%s' (%s)" % (_fmt(data), _format_bytes(data)))
        data = data.strip()
        data = _strip_unprintables(data) # eliminate random NULL characters
        _check_crc(data)
        return data

    def flush(self):
        self.flush_input()
        self.flush_output()

    def flush_input(self):
        logdbg("flush input buffer")
        self.serial_port.flushInput()

    def flush_output(self):
        logdbg("flush output buffer")
        self.serial_port.flushOutput()

    def queued_bytes(self):
        return self.serial_port.inWaiting()

    def send_cmd(self, cmd):
        """Any command must be terminated with a CR"""
        self.write("%s\r" % cmd)

    def command(self, cmd):
        self.send_cmd(cmd)
        data = self.read()
        if data != cmd:
            raise weewx.WeeWxIOError("Command failed: cmd='%s' reply='%s' (%s)"
                                     % (cmd, _fmt(data), _format_bytes(data)))
        return self.read()

    def get_version(self):
        logdbg("get firmware version")
        return self.command("VERSION")

    # give the station some time to wake up.  when we first hit it with a
    # command, it often responds with an empty string.  then subsequent
    # commands get the proper response.  so for a first command, send something
    # innocuous and wait a bit.  hopefully subsequent commands will then work.
    def wakeup(self):
        self.command('ECHO=?')
        time.sleep(1.0)

    def set_echo(self, cmd='ON'):
        logdbg("set echo to %s" % cmd)
        data = self.command('ECHO=%s' % cmd)
        if data != 'OK':
            raise weewx.WeeWxIOError("Set ECHO failed: %s" % _fmt(data))

    def get_header(self):
        logdbg("get header")
        data = self.command("HEADER")
        cols = data.split(',')
        if cols[0] != 'HDR':
            raise weewx.WeeWxIOError("Expected HDR, got %s" % cols[0])
        return cols

    def set_auto(self):
        # auto does not echo the command
        self.send_cmd("AUTO")

    def get_current_data(self, send_now=True):
        data = ''
        if send_now:
            data = self.command("NOW")
        else:
            data = self.read()
        if data == 'NO DATA' or data == 'NO DATA RECEIVED':
            loginf("No data from sensors")
            return []
        return data.split(',')

    def get_time(self):
        # unlike all of the other accessor methods, the TIME command returns
        # OK after it returns the requested parameter.  so we have to pop the
        # OK off the serial so it does not trip up other commands.
        logdbg("get time")
        tstr = self.command("TIME=?")
        if tstr not in ['ERROR', 'OK']:
            data = self.read()
        if data != 'OK':
            raise weewx.WeeWxIOError("Failed to get time: %s" % _fmt(data))
        return tstr

    def set_time(self):
        ts = time.time()
        tstr = time.strftime("%Y/%m/%d %H:%M:%S", time.localtime(ts))
        logdbg("set time to %s (%s)" % (tstr, ts))
        s = "TIME=%s" % tstr
        data = self.command(s)
        if data != 'OK':
            raise weewx.WeeWxIOError("Failed to set time to %s: %s" %
                                     (s, _fmt(data)))

    def get_dst(self):
        logdbg("get daylight saving")
        return self.command("DST=?")

    def set_dst(self, dst):
        logdbg("set DST to %s" % dst)
        data = self.command("DST=%s" % dst)
        # FIXME: firmware 1.3 Build 016 Aug 21 2014 does not return OK
        if data != 'OK':
            raise weewx.WeeWxIOError("Failed to set DST to %s: %s" %
                                     (dst, _fmt(data)))

    def get_units(self):
        logdbg("get units")
        return self.command("UNITS=?")

    def set_units(self, units):
        logdbg("set units to %s" % units)
        data = self.command("UNITS=%s" % units)
        if data != 'OK':
            raise weewx.WeeWxIOError("Failed to set units to %s: %s" %
                                     (units, _fmt(data)))

    def get_interval(self):
        logdbg("get logging interval")
        return int(self.command("LOGINT=?"))

    def set_interval(self, interval=5):
        logdbg("set logging interval to %d minutes" % interval)
        data = self.command("LOGINT=%d" % interval)
        if data != 'OK':
            raise weewx.WeeWxIOError("Failed to set logging interval: %s" %
                                     _fmt(data))

    def get_channel(self):
        logdbg("get channel")
        return self.command("STATION")

    def set_channel(self, channel):
        logdbg("set channel to %d" % channel)
        if channel < 0 or 3 < channel:
            raise ValueError("Channel must be 0-3")
        data = self.command("STATION=%d" % channel)
        if data != 'OK':
            raise weewx.WeeWxIOError("Failed to set channel: %s" % _fmt(data))

    def get_charger(self):
        logdbg("get charger")
        return self.command("CHARGER")

    def get_baro(self):
        logdbg("get baro")
        return self.command("BARO")

    def set_baro(self, offset):
        logdbg("set barometer offset to %d" % offset)
        if offset != '0':
            parts = offset.split('.')
            if (len(parts) != 2 or
                (not (len(parts[0]) == 2 and len(parts[1]) == 2) and
                 not (len(parts[0]) == 3 and len(parts[1]) == 1))):
                raise ValueError("Offset must be 0, XX.XX (inHg), or XXXX.X (mbar)")
        data = self.command("BARO=%d" % offset)
        if data != 'OK':
            raise weewx.WeeWxIOError("Failed to set baro: %s" % _fmt(data))

    def get_memory_status(self):
        logdbg("get memory status")
        return self.command("MEM=?")

    def clear_memory(self):
        logdbg("clear memory")
        data = self.command("MEM=CLEAR")
        # FIXME: firmware 1.3 Build 016 Aug 21 2014 does not return OK
        if data != 'OK':
            raise weewx.WeeWxIOError("Failed to clear memory: %s" % _fmt(data))

    def get_rain(self):
        logdbg("get rain total")
        return self.command("RAIN")

    def reset_rain(self):
        logdbg("reset rain counter")
        data = self.command("RAIN=RESET")
        if data != 'OK':
            raise weewx.WeeWxIOError("Failed to reset rain: %s" % _fmt(data))

    def gen_records(self, nrec=0):
        """generator function for getting nrec records from the device"""

        logdbg("download %s records" % nrec)
        need_cmd = True
        cmd_max = 5
        cmd_cnt = 0
        n = 0
        while True:
            if need_cmd:
                if cmd_cnt >= cmd_max:
                    logerr("download aborted after %d attempts" % cmd_max)
                    break
                cmd_cnt += 1
                qty = nrec - n if nrec else 0 # FIXME
                loginf("download attempt %s of %s" % (cmd_cnt, cmd_max))
                cmd = "DOWNLOAD=%d" % qty if qty else "DOWNLOAD"
                self.send_cmd(cmd)
                need_cmd = False
            try:
                data = self.read()
                if data == 'OK':
                    logdbg("end of records")
                    break
                values = data.split(',')
                if values[0] == 'REC':
                    logdbg("record %d" % n)
                    n += 1
                    cmd_cnt = 0
                    yield values
                elif (values[0] == 'HDR' or values[0] == 'MSG' or
                      values[0] == 'MIN' or values[0] == 'MAX' or
                      values[0].startswith('DOWNLOAD')):
                    pass
                elif values[0] == '':
                    # FIXME: this causes 'input overrun' on rpi2 with debian 7
                    loginf("download hung, initiate another download")
                    need_cmd = True
                else:
                    logerr("bad record %s '%s' (%s)" %
                           (n, _fmt(values[0]), _fmt(data)))
            except ChecksumError, e:
                logerr("download failed for record %s: %s" % (n, e))


class CC3000ConfEditor(weewx.drivers.AbstractConfEditor):
    @property
    def default_stanza(self):
        return """
[CC3000]
    # This section is for RainWise MarkIII weather stations and CC3000 logger.

    # Serial port such as /dev/ttyS0, /dev/ttyUSB0, or /dev/cuaU0
    port = %s

    # The station model, e.g., CC3000 or CC3000R
    model = CC3000

    # The driver to use:
    driver = weewx.drivers.cc3000
""" % (CC3000.DEFAULT_PORT,)

    def prompt_for_settings(self):
        print "Specify the serial port on which the station is connected, for"
        print "example /dev/ttyUSB0 or /dev/ttyS0."
        port = self._prompt('port', CC3000.DEFAULT_PORT)
        return {'port': port}


# define a main entry point for basic testing without weewx engine and service
# overhead.  invoke this as follows from the weewx root dir:
#
# PYTHONPATH=bin python bin/weewx/drivers/cc3000.py

if __name__ == '__main__':
    import optparse

    usage = """%prog [options] [--help]"""

    syslog.openlog('cc3000', syslog.LOG_PID | syslog.LOG_CONS)
    syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_INFO))
    parser = optparse.OptionParser(usage=usage)
    parser.add_option('--version', dest='version', action='store_true',
                      help='display driver version')
    parser.add_option('--debug', dest='debug', action='store_true',
                      default=False,
                      help='emit additional diagnostic information')
    parser.add_option('--test-crc', dest='testcrc', action='store_true',
                      help='test crc')
    parser.add_option('--port', dest='port', metavar='PORT',
                      help='port to which the station is connected',
                      default=CC3000.DEFAULT_PORT)
    parser.add_option('--get-version', dest='getver', action='store_true',
                      help='display firmware version')
    parser.add_option('--get-status', dest='status', action='store_true',
                      help='display memory status')
    parser.add_option('--get-channel', dest='getch', action='store_true',
                      help='display station channel')
    parser.add_option('--set-channel', dest='setch', metavar='CHANNEL',
                      help='set station channel')
    parser.add_option('--get-battery', dest='getbat', action='store_true',
                      help='display battery status')
    parser.add_option('--get-current', dest='getcur', action='store_true',
                      help='display current data')
    parser.add_option('--get-memory', dest='getmem', action='store_true',
                      help='display memory status')
    parser.add_option('--get-records', dest='getrec', metavar='NUM_RECORDS',
                      help='display records from station memory')
    parser.add_option('--get-header', dest='gethead', action='store_true',
                      help='display data header')
    parser.add_option('--get-units', dest='getunits', action='store_true',
                      help='display units')
    parser.add_option('--set-units', dest='setunits', metavar='UNITS',
                      help='set units to ENGLISH or METRIC')
    parser.add_option('--get-time', dest='gettime', action='store_true',
                      help='display station time')
    parser.add_option('--set-time', dest='settime', action='store_true',
                      help='set station time to computer time')
    parser.add_option('--get-dst', dest='getdst', action='store_true',
                      help='display daylight savings settings')
    parser.add_option('--set-dst', dest='setdst',
                      metavar='mm/dd HH:MM,mm/dd HH:MM,[MM]M',
                      help='set daylight savings start, end, and amount')
    parser.add_option('--get-interval', dest='getint', action='store_true',
                      help='display logging interval, in seconds')
    parser.add_option('--set-interval', dest='setint', metavar='INTERVAL',
                      type=int, help='set logging interval, in seconds')
    parser.add_option('--clear-memory', dest='clear', action='store_true',
                      help='clear logger memory')
    parser.add_option('--reset-rain', dest='reset', action='store_true',
                      help='reset rain counter')
    parser.add_option('--poll', dest='poll', metavar='POLL_INTERVAL', type=int,
                      help='poll interval in seconds')
    (options, args) = parser.parse_args()

    if options.version:
        print "CC3000 driver version %s" % DRIVER_VERSION
        exit(0)

    if options.debug:
        DEBUG_SERIAL = 1
        DEBUG_CHECKSUM = 1
        DEBUG_OPENCLOSE = 1
        syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_DEBUG))

    if options.testcrc:
        _check_crc('OK')
        _check_crc('REC,2010/01/01 14:12, 64.5, 85,29.04,349,  2.4,  4.2,  0.00, 6.21, 0.25, 73.2,!B82C')
        _check_crc('MSG,2010/01/01 20:22,CHARGER ON,!4CED')
        exit(0)

    with CC3000(options.port) as s:
        s.flush()
        s.wakeup()
        s.set_echo()
        if options.getver:
            print s.get_version()
        if options.status:
            print "firmware:", s.get_version()
            print "time:", s.get_time()
            print "dst:", s.get_dst()
            print "units:", s.get_units()
            print "memory:", s.get_memory_status()
            print "interval:", s.get_interval() * 60
            print "channel:", s.get_channel()
            print "charger:", s.get_charger()
            print "baro:", s.get_baro()
            print "rain:", s.get_rain()
        if options.getch:
            print s.get_channel()
        if options.setch is not None:
            s.set_channel(int(options.setch))
        if options.getbat:
            print s.get_charger()
        if options.getcur:
            print s.get_current_data()
        if options.getmem:
            print s.get_memory_status()
        if options.getrec is not None:
            i = 0
            for r in s.gen_records(int(options.getrec)):
                print i, r
                i += 1
        if options.gethead:
            print s.get_header()
        if options.getunits:
            print s.get_units()
        if options.setunits:
            s.set_units(options.setunits)
        if options.gettime:
            print s.get_time()
        if options.settime:
            s.set_time()
        if options.getdst:
            print s.get_dst()
        if options.setdst:
            s.set_dst(options.setdst)
        if options.getint:
            print s.get_interval() * 60
        if options.setint:
            s.set_interval(int(options.setint) / 60)
        if options.clear:
            s.clear_memory()
        if options.reset:
            s.reset_rain()
        if options.poll is not None:
            poll = int(options.poll)
            cmd_mode = True
            if poll == 0:
                cmd_mode = False
                s.set_auto()
            while True:
                print s.get_current_data(cmd_mode)
                time.sleep(poll)
