#!/usr/bin/env python
#
# Copyright 2014-2023 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.

The historical records (DOWNLOAD), as well as current readings (NOW) track
the amount of rain since midnight; i.e., DOWNLOAD records rain value resets to 0
at midnight and NOW records do the same.

The RAIN=? returns a rain counter that only resets with the RAIN=RESET command.
This counter isn't used by weewx.  Also, RAIN=RESET doesn't just reset this
counter, it also resets the daily rain count.

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

The CC3000 has the habit of failing to execute about 1 in 6000
commands.  That the bad news.  The good news is that the
condition is easily detected and the driver can recover in about 1s.
The telltale sing of failure is the first read after sending
the command (to read the echo of the command) times out.  As such,
the timeout is set to 1s.  If the timeout is hit, the buffers
are flushed and the command is retried.  Oh, and there is one
more pecurliar part to this.  On the retry, the command is echoed
as an empty string.  That empty string is expected on the retry
and execution continues.

weewx includes a logwatch script that makes it easy to see the above
behavior in action.  In the snippet below, 3 NOW commands and one
IME=? were retried successfully.  The Retry Info section shows
that all succeeded on the second try.
 --------------------- weewx Begin ------------------------

 average station clock skew: 0.0666250000000001
   min: -0.53 max: 0.65 samples: 160

 counts:
   archive: records added                           988
   cc3000: NOW cmd echo timed out                     3
   cc3000: NOW echoed as empty string                 3
   cc3000: NOW successful retries                     3
   cc3000: TIME=? cmd echo timed out                  1
   cc3000: TIME=? echoed as empty string              1
   cc3000: TIME=? successful retries                  1
   ....
 cc3000 Retry Info:
   Dec 29 00:50:04 ella weewx[24145] INFO weewx.drivers.cc3000: TIME=?: Retry worked.  Total tries: 2
   Dec 29 04:46:21 ella weewx[24145] INFO weewx.drivers.cc3000: NOW: Retry worked.  Total tries: 2
   Dec 29 08:31:11 ella weewx[22295] INFO weewx.drivers.cc3000: NOW: Retry worked.  Total tries: 2
   Dec 29 08:50:51 ella weewx[22295] INFO weewx.drivers.cc3000: NOW: Retry worked.  Total tries: 2
   ....
 ---------------------- weewx End -------------------------


Clearing memory on the CC3000 takes about 12s.  As such, the 1s
timeout mentioned above won't work for this command.  Consequently,
when executing MEM=CLEAR, the timeout is set to 20s.  Should this
command fail, rather than losing 1 second retrying, 20 sexconds
will be lost.


The CC3000 very rarely stops returning observation values.
[Observed once in 28 months of operation over two devices.]
Operation returns to normal after the CC3000 is rebooted.
This driver now reboots when this situation  is detected.
If this happens, the log will show:
    INFO weewx.drivers.cc3000: No data from sensors, rebooting.
    INFO weewx.drivers.cc3000: Back from a reboot:
    INFO weewx.drivers.cc3000: ....................
    INFO weewx.drivers.cc3000:
    INFO weewx.drivers.cc3000: Rainwise CC-3000 Version: 1.3 Build 022 Dec 02 2016
    INFO weewx.drivers.cc3000: Flash ID 202015
    INFO weewx.drivers.cc3000: Initializing memory...OK.

This driver was tested with:
  Rainwise CC-3000 Version: 1.3 Build 022 Dec 02 2016

Earlier versions of this driver were 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: Come up with a way to deal with firmware inconsistencies.  if we do
#        a strict protocol where we wait for an OK response, but one version of
#        the firmware responds whereas another version does not, this leads to
#        comm problems.  specializing the code to handle quirks of each
#        firmware version is not desirable.
#        UPDATE: As of 0.30, the driver does a flush of the serial buffer before
#        doing any command.  The problem detailed above (OK not being returned)
#        was probably because the timeout was too short for the MEM=CLEAR
#        command.  That command gets a longer timeout in version 0.30.

# FIXME: Figure out why system log messages are lost.  When reading from the logger
#        there are many messages to the log that just do not show up, or msgs
#        that appear in one run but not in a second, identical run.  I suspect
#        that system log cannot handle the load?  or its buffer is not big enough?
#        Update:
#        With debug=0, this has never been observed in v1.3 Build 22 Dec 02 2016.
#        With debug=1, tailing the log looks like everything is running, but no
#        attempt was made to compuare log data between runs.  Observations on
#        NUC7i5 running Debian Buster.

import datetime
import logging
import math
import time

import serial

import weeutil.weeutil
import weewx.drivers
import weewx.wxformulas
from weeutil.weeutil import to_int
from weewx.crc16 import crc16

log = logging.getLogger(__name__)

DRIVER_NAME = 'CC3000'
DRIVER_VERSION = '0.5'

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


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, 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, 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("--get-header", dest="gethead", action="store_true",
                          help="display data header")
        parser.add_option("--get-rain", dest="getrain", action="store_true",
                          help="get the rain counter")
        parser.add_option("--reset-rain", dest="resetrain", action="store_true",
                          help="reset the rain counter")
        parser.add_option("--get-max", dest="getmax", action="store_true",
                          help="get the max values observed")
        parser.add_option("--reset-max", dest="resetmax", action="store_true",
                          help="reset the max counters")
        parser.add_option("--get-min", dest="getmin", action="store_true",
                          help="get the min values observed")
        parser.add_option("--reset-min", dest="resetmin", action="store_true",
                          help="reset the min counters")
        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 seconds")
        parser.add_option("--set-interval", dest="interval", metavar="N",
                          type=int,
                          help="set logging interval to N seconds")
        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='setdst',
                          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):  # @UnusedVariable
        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(options.nrecords):
                print(r)
        elif options.nminutes is not None:
            since_ts = time.mktime((datetime.datetime.now()-datetime.timedelta(
                                   minutes=options.nminutes)).timetuple())
            for r in self.driver.gen_records_since_ts(since_ts):
                print(r)
        elif options.clear:
            self.clear_memory(options.noprompt)
        elif options.gethead:
            print(self.driver.station.get_header())
        elif options.getrain:
            print(self.driver.station.get_rain())
        elif options.resetrain:
            self.reset_rain(options.noprompt)
        elif options.getmax:
            print(self.driver.station.get_max())
        elif options.resetmax:
            self.reset_max(options.noprompt)
        elif options.getmin:
            print(self.driver.station.get_min())
        elif options.resetmin:
            self.reset_min(options.noprompt)
        elif options.getclock:
            print(self.driver.station.get_time())
        elif options.setclock:
            self.set_clock(options.noprompt)
        elif options.getdst:
            print(self.driver.station.get_dst())
        elif options.setdst:
            self.set_dst(options.setdst, options.noprompt)
        elif options.getint:
            print(self.driver.station.get_interval() * 60)
        elif options.interval is not None:
            self.set_interval(options.interval / 60, options.noprompt)
        elif options.getunits:
            print(self.driver.station.get_units())
        elif options.units is not None:
            self.set_units(options.units, options.noprompt)
        elif options.getch:
            print(self.driver.station.get_channel())
        elif options.ch is not None:
            self.set_channel(options.ch, options.noprompt)
        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())
            print("Rain:", self.driver.station.get_rain())
            print("HEADER:", self.driver.station.get_header())
            print("MAX:", self.driver.station.get_max())
            print("MIN:", self.driver.station.get_min())
        self.driver.closePort()

    def clear_memory(self, noprompt):
        print(self.driver.station.get_memory_status())
        ans = weeutil.weeutil.y_or_n("Clear console memory (y/n)? ",
                                     noprompt)
        if ans == 'y':
            print('Clearing memory (takes approx. 12s)')
            self.driver.station.clear_memory()
            print(self.driver.station.get_memory_status())
        else:
            print("Clear memory cancelled.")

    def reset_rain(self, noprompt):
        print(self.driver.station.get_rain())
        ans = weeutil.weeutil.y_or_n("Reset rain counter (y/n)? ",
                                     noprompt)
        if ans == 'y':
            print('Resetting rain counter')
            self.driver.station.reset_rain()
            print(self.driver.station.get_rain())
        else:
            print("Reset rain cancelled.")

    def reset_max(self, noprompt):
        print(self.driver.station.get_max())
        ans = weeutil.weeutil.y_or_n("Reset max counters (y/n)? ",
                                     noprompt)
        if ans == 'y':
            print('Resetting max counters')
            self.driver.station.reset_max()
            print(self.driver.station.get_max())
        else:
            print("Reset max cancelled.")

    def reset_min(self, noprompt):
        print(self.driver.station.get_min())
        ans = weeutil.weeutil.y_or_n("Reset min counters (y/n)? ",
                                     noprompt)
        if ans == 'y':
            print('Resetting min counters')
            self.driver.station.reset_min()
            print(self.driver.station.get_min())
        else:
            print("Reset min cancelled.")

    def set_interval(self, interval, noprompt):
        if interval < 0 or 60 < interval:
            raise ValueError("Logger interval must be 0-60 minutes")
        print("Interval is", self.driver.station.get_interval(), " minutes.")
        ans = weeutil.weeutil.y_or_n("Set interval to %d minutes (y/n)? " % interval,
                                     noprompt)
        if ans == 'y':
            print("Setting interval to %d minutes" % interval)
            self.driver.station.set_interval(interval)
            print("Interval is now", self.driver.station.get_interval())
        else:
            print("Set interval cancelled.")

    def set_clock(self, noprompt):
        print("Station clock is", self.driver.station.get_time())
        print("Current time is", datetime.datetime.now())
        ans = weeutil.weeutil.y_or_n("Set station time to current time (y/n)? ",
                                     noprompt)
        if ans == 'y':
            print("Setting station clock to %s" % datetime.datetime.now())
            self.driver.station.set_time()
            print("Station clock is now", self.driver.station.get_time())
        else:
            print("Set clock cancelled.")

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

    def set_dst(self, dst, noprompt):
        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")
        print("Station DST is", self.driver.station.get_dst())
        ans = weeutil.weeutil.y_or_n("Set station DST to %s (y/n)? " % dst,
                                     noprompt)
        if ans == 'y':
            print("Setting station DST to %s" % dst)
            self.driver.station.set_dst(dst)
            print("Station DST is now", self.driver.station.get_dst())
        else:
            print("Set DST cancelled.")

    def set_channel(self, ch, noprompt):
        if ch not in [0, 1, 2, 3]:
            raise ValueError("Channel must be one of 0, 1, 2, or 3")
        print("Station channel is", self.driver.station.get_channel())
        ans = weeutil.weeutil.y_or_n("Set station channel to %s (y/n)? " % ch,
                                     noprompt)
        if ans == 'y':
            print("Setting station channel to %s" % ch)
            self.driver.station.set_channel(ch)
            print("Station channel is now", self.driver.station.get_channel())
        else:
            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',
        'supplyVoltage': 'STATION BATTERY',
        'consBatteryVoltage': 'BATTERY BACKUP',
        'radiation': 'SOLAR RADIATION',
        'UV': 'UV INDEX',
    }

    def __init__(self, **stn_dict):
        log.info('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.max_tries = int(stn_dict.get('max_tries', 5))
        self.model = stn_dict.get('model', 'CC3000')
        port = stn_dict.get('port', CC3000.DEFAULT_PORT)
        log.info('Using serial port %s' % port)
        self.polling_interval = float(stn_dict.get('polling_interval', 2))
        log.info('Polling interval is %s seconds' % self.polling_interval)
        self.use_station_time = weeutil.weeutil.to_bool(
            stn_dict.get('use_station_time', True))
        log.info('Using %s time for loop packets' %
               ('station' if self.use_station_time else 'computer'))
        # start with the default sensormap, then augment with user-specified
        self.sensor_map = dict(self.DEFAULT_SENSOR_MAP)
        if 'sensor_map' in stn_dict:
            self.sensor_map.update(stn_dict['sensor_map'])
        log.info('Sensor map is %s' % self.sensor_map)

        # periodically check the logger memory, then clear it if necessary.
        # these track the last time a check was made, and how often to make
        # the checks.  threshold of None indicates do not clear logger.
        self.logger_threshold = to_int(
            stn_dict.get('logger_threshold', 0))
        self.last_mem_check = 0
        self.mem_interval = 7 * 24 * 3600
        if self.logger_threshold != 0:
            log.info('Clear logger at %s records' % self.logger_threshold)

        # track the last rain counter value so we can determine deltas
        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)
        log.info('Firmware: %s' % settings['firmware'])
        self.arcint = settings['arcint']
        log.info('Archive interval: %s' % self.arcint)
        self.header = settings['header']
        log.info('Header: %s' % self.header)
        self.units = weewx.METRICWX if settings['units'] == 'METRIC' else weewx.US
        log.info('Units: %s' % settings['units'])
        log.info('Channel: %s' % settings['channel'])
        log.info('Charger status: %s' % settings['charger'])
        log.info('Memory: %s' % self.station.get_memory_status())

    def time_to_next_poll(self):
        now = time.time()
        next_poll_event = int(now / self.polling_interval) * self.polling_interval + self.polling_interval
        log.debug('now: %f, polling_interval: %d, next_poll_event: %f' % (now, self.polling_interval, next_poll_event))
        secs_to_poll = next_poll_event - now
        log.debug('Next polling event in %f seconds' % secs_to_poll)
        return secs_to_poll

    def genLoopPackets(self):
        cmd_mode = True
        if self.polling_interval == 0:
            self.station.set_auto()
            cmd_mode = False

        reboot_attempted = False
        ntries = 0
        while ntries < self.max_tries:
            ntries += 1
            try:
                # Poll on polling_interval boundaries.
                if self.polling_interval != 0:
                    time.sleep(self.time_to_next_poll())
                values = self.station.get_current_data(cmd_mode)
                now = int(time.time())
                ntries = 0
                log.debug("Values: %s" % values)
                if values:
                    packet = self._parse_current(
                        values, self.header, self.sensor_map)
                    log.debug("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:
                            log.debug("No rain in packet: %s" % packet)
                        log.debug("Packet: %s" % packet)
                        yield packet
                else:
                    if not reboot_attempted:
                        # To be on the safe side, max of one reboot per execution.
                        reboot_attempted = True
                        log.info("No data from sensors, rebooting.")
                        startup_msgs = self.station.reboot()
                        log.info("Back from a reboot:")
                        for line in startup_msgs:
                            log.info(line)

                # periodically check memory, clear if necessary
                if time.time() - self.last_mem_check > self.mem_interval:
                    nrec = self.station.get_history_usage()
                    self.last_mem_check = time.time()
                    if nrec is None:
                        log.info("Memory check: Cannot determine memory usage")
                    else:
                        log.info("Logger is at %d records, "
                               "logger clearing threshold is %d" %
                               (nrec, self.logger_threshold))
                        if self.logger_threshold != 0 and nrec >= self.logger_threshold:
                            log.info("Clearing all records from logger")
                            self.station.clear_memory()
            except (serial.serialutil.SerialException, weewx.WeeWxIOError) as e:
                log.error("Failed attempt %d of %d to get data: %s" %
                       (ntries, self.max_tries, e))
        else:
            msg = "Max retries (%d) exceeded" % self.max_tries
            log.error(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
        """
        log.debug("GenStartupRecords: since_ts=%s" % since_ts)
        log.info('Downloading new records (if any).')
        last_rain = None
        new_records = 0
        for pkt in self.gen_records_since_ts(since_ts):
            log.debug("Packet: %s" % pkt)
            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:
                log.debug("No rain in record: %s" % r)
            log.debug("Packet: %s" % pkt)
            new_records += 1
            yield pkt
        log.info('Downloaded %d new records.' % new_records)

    def gen_records_since_ts(self, since_ts):
        return self.station.gen_records_since_ts(self.header, self.sensor_map, since_ts)

    @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 as e:
            log.error("getTime failed: %s" % e)
        return 0

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

    @staticmethod
    def _init_station_with_retries(station, max_tries):
        for cnt in range(max_tries):
            try:
                return CC3000Driver._init_station(station)
            except (serial.serialutil.SerialException, weewx.WeeWxIOError) as e:
                log.error("Failed attempt %d of %d to initialize station: %s" %
                       (cnt + 1, max_tries, e))
        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 between the current and previous rain totals.
        return weewx.wxformulas.calculate_rain(rain_total, last_rain)

    @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_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:
            log.info("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 as e:
                log.error("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' % c for c in buf])

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

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

    def __init__(self, port):
        self.port = port
        self.baudrate = 115200
        self.timeout = 1 # seconds for everyting except MEM=CLEAR
        # MEM=CLEAR of even two records needs a timeout of 13 or more.  20 is probably safe.
        #           flush    cmd      echo      value
        #           0.000022 0.000037 12.819934 0.000084
        #           0.000018 0.000036 12.852024 0.000088
        self.mem_clear_timeout = 20 # reopen w/ bigger  timeout for MEM=CLEAR
        self.serial_port = None

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

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

    def open(self, timeoutOverride=None):
        if DEBUG_OPENCLOSE:
            log.debug("Open serial port %s" % self.port)
        to = timeoutOverride if timeoutOverride is not None else self.timeout
        self.serial_port = serial.Serial(self.port, self.baudrate,
                                         timeout=to)

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

    def write(self, data):
        # Encode could perhaps fail on bad user input (DST?).
        # If so, this will be handled later when it is observed that the
        # command does not do what is expected.
        data = data.encode('ascii', 'ignore')
        if DEBUG_SERIAL:
            log.debug("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:
            log.debug("Read: '%s' (%s)" % (data, _format_bytes(data)))
        data = data.strip()
        _check_crc(data)
        # CRC passed, so this is unlikely.
        # Ignore as irregular data will be handled later.
        data = data.decode('ascii', 'ignore')
        return data

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

    def flush_input(self):
        log.debug("Flush input buffer")
        self.serial_port.flushInput()

    def flush_output(self):
        log.debug("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):
        # Sample timings for first fifteen NOW commands after startup.
        #   Flush     CMD     ECHO     VALUE
        # -------- -------- -------- --------
        # 0.000021 0.000054 0.041557 0.001364
        # 0.000063 0.000109 0.040432 0.001666
        # 0.000120 0.000123 0.024272 0.016871
        # 0.000120 0.000127 0.025148 0.016657
        # 0.000119 0.000126 0.024966 0.016665
        # 0.000130 0.000142 0.041037 0.001791
        # 0.000120 0.000126 0.023533 0.017023
        # 0.000120 0.000137 0.024336 0.016747
        # 0.000117 0.000133 0.026254 0.016684
        # 0.000120 0.000140 0.025014 0.016739
        # 0.000121 0.000134 0.024801 0.016779
        # 0.000120 0.000141 0.024635 0.016906
        # 0.000118 0.000129 0.024354 0.016894
        # 0.000120 0.000133 0.024214 0.016861
        # 0.000118 0.000122 0.024599 0.016865

        # MEM=CLEAR needs a longer timeout.  >12s to clear a small number of records has been observed.
        # It also appears to be highly variable.  The two examples below are from two different CC3000s.
        #
        # In this example, clearing at 11,595 records took > 6s.
        # Aug 18 06:46:21 charlemagne weewx[684]: cc3000: logger is at 11595 records, logger clearing threshold is 10000
        # Aug 18 06:46:21 charlemagne weewx[684]: cc3000: clearing all records from logger
        # Aug 18 06:46:21 charlemagne weewx[684]: cc3000: MEM=CLEAR: The resetting of timeout to 20 took 0.000779 seconds.
        # Aug 18 06:46:28 charlemagne weewx[684]: cc3000: MEM=CLEAR: times: 0.000016 0.000118 6.281638 0.000076
        # Aug 18 06:46:28 charlemagne weewx[684]: cc3000: MEM=CLEAR: The resetting of timeout to 1 took 0.001444 seconds.
        #
        # In this example, clearing at 11,475 records took > 12s.
        # Aug 18 07:17:14 ella weewx[615]: cc3000: logger is at 11475 records, logger clearing threshold is 10000
        # Aug 18 07:17:14 ella weewx[615]: cc3000: clearing all records from logger
        # Aug 18 07:17:14 ella weewx[615]: cc3000: MEM=CLEAR: The resetting of timeout to 20 took 0.001586 seconds.
        # Aug 18 07:17:27 ella weewx[615]: cc3000: MEM=CLEAR: times: 0.000020 0.000058 12.459346 0.000092
        # Aug 18 07:17:27 ella weewx[615]: cc3000: MEM=CLEAR: The resetting of timeout to 1 took 0.001755 seconds.
        #
        # Here, clearing 90 records took very close to 13 seconds.
        # Aug 18 14:46:00 ella weewx[24602]: cc3000: logger is at 91 records, logger clearing threshold is 90
        # Aug 18 14:46:00 ella weewx[24602]: cc3000: clearing all records from logger
        # Aug 18 14:46:00 ella weewx[24602]: cc3000: MEM=CLEAR: The resetting of timeout to 20 took 0.000821 seconds.
        # Aug 18 14:46:13 ella weewx[24602]: cc3000: MEM=CLEAR: times: 0.000037 0.000061 12.970494 0.000084
        # Aug 18 14:46:13 ella weewx[24602]: cc3000: MEM=CLEAR: The resetting of timeout to 1 took 0.001416 seconds.

        reset_timeout = False

        # MEM=CLEAR needs a much larger timeout value.  Reopen with that larger timeout and reset below.
        #
        # Closing and reopening with a different timeout is quick:
        #     Aug 18 07:17:14 ella weewx[615]: cc3000: MEM=CLEAR: The resetting of timeout to 20 took 0.001586 seconds.
        #     Aug 18 07:17:27 ella weewx[615]: cc3000: MEM=CLEAR: The resetting of timeout to 1 took 0.001755 seconds.
        if cmd == 'MEM=CLEAR':
            reset_timeout = True # Reopen with default timeout in finally.
            t1 = time.time()
            self.close()
            self.open(self.mem_clear_timeout)
            t2 = time.time()
            close_open_time = t2 - t1
            log.info("%s: The resetting of timeout to %d took %f seconds." % (cmd, self.mem_clear_timeout, close_open_time))

        try:
            return self.exec_cmd_with_retries(cmd)
        finally:
            if reset_timeout:
                t1 = time.time()
                self.close()
                self.open()
                reset_timeout = True
                t2 = time.time()
                close_open_time = t2 - t1
                log.info("%s: The resetting of timeout to %d took %f seconds." % (cmd, self.timeout, close_open_time))

    def exec_cmd_with_retries(self, cmd):
        """Send cmd.  Time the reading of the echoed command.  If the measured
        time is >= timeout, the cc3000 is borked.  The input and output buffers
        will be flushed and the command retried.  Try up to 10 times.
        It practice, one retry does the trick.
        cc3000s.
        """
        attempts = 0
        while attempts < 10:
            attempts += 1
            t1 = time.time()
            self.flush()          # flush
            t2 = time.time()
            flush_time = t2 - t1
            self.send_cmd(cmd)    # send cmd
            t3 = time.time()
            cmd_time = t3 - t2
            data = self.read()    # read the cmd echo
            t4 = time.time()
            echo_time = t4 - t3

            if ((cmd != 'MEM=CLEAR' and echo_time >= self.timeout)
                    or (cmd == 'MEM=CLEAR' and echo_time >= self.mem_clear_timeout)):
                # The command timed out reading back the echo of the command.
                # No need to read the values as it will also time out.
                # Log it and retry.  In practice, the retry always works.
                log.info("%s: times: %f %f %f -retrying-" %
                         (cmd, flush_time, cmd_time, echo_time))
                log.info('%s: Reading cmd echo timed out (%f seconds), retrying.' %
                         (cmd, echo_time))
                # Retrying setting the time must be special cased as now a little
                # more than one second has passed.  As such, redo the command
                # with the current time.
                if cmd.startswith("TIME=") and cmd != "TIME=?":
                    cmd = self._compose_set_time_command()
                # Retry
            else:
                # Success, the reading of the echoed command did not time out.
                break

        if data != cmd and attempts > 1:
            # After retrying, the cmd always echoes back as an empty string.
            if data == '':
                log.info("%s: Accepting empty string as cmd echo." % cmd)
            else:
                raise weewx.WeeWxIOError(
                    "command: Command failed: cmd='%s' reply='%s'" % (cmd, data))

        t5 = time.time()
        retval = self.read()
        t6 = time.time()
        value_time = t6 - t5
        if cmd == 'MEM=CLEAR':
            log.info("%s: times: %f %f %f %f" %
                     (cmd, flush_time, cmd_time, echo_time, value_time))

        if attempts > 1:
            if retval != '':
                log.info("%s: Retry worked.  Total tries: %d" % (cmd, attempts))
            else:
                log.info("%s: Retry failed." % cmd)
            log.info("%s: times: %f %f %f %f" %
                     (cmd, flush_time, cmd_time, echo_time, value_time))

        return retval

    def get_version(self):
        log.debug("Get firmware version")
        return self.command("VERSION")

    def reboot(self):
        # Reboot outputs the following (after the reboot):
        # ....................
        # <blank line>
        # Rainwise CC-3000 Version: 1.3 Build 022 Dec 02 2016
        # Flash ID 202015
        # Initializing memory...OK.
        log.debug("Rebooting CC3000.")
        self.send_cmd("REBOOT")
        time.sleep(5)
        dots = self.read()
        blank = self.read()
        ver = self.read()
        flash_id = self.read()
        init_msg = self.read()
        return [dots, blank, ver, flash_id, init_msg]

    # 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.
    # NOTE: This happens periodically and does not appear to be related to
    # "waking up".  Getter commands now retry, so removing the sleep.
    def wakeup(self):
        self.command('ECHO=?')

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

    def get_header(self):
        log.debug("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':
            log.debug("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.
        log.debug("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, %s" % (tstr, data))
        return tstr

    @staticmethod
    def _compose_set_time_command():
        ts = time.time()
        tstr = time.strftime("%Y/%m/%d %H:%M:%S", time.localtime(ts))
        log.info("Set time to %s (%s)" % (tstr, ts))
        return "TIME=%s" % tstr

    def set_time(self):
        s = self._compose_set_time_command()
        data = self.command(s)
        if data != 'OK':
            raise weewx.WeeWxIOError("Failed to set time to %s: %s" %
                                     (s, data))

    def get_dst(self):
        log.debug("Get daylight saving")
        return self.command("DST=?")

    def set_dst(self, dst):
        log.debug("Set DST to %s" % dst)
        # Firmware 1.3 Build 022 Dec 02 2016 returns 3 lines (<input-dst>,'',OK)
        data = self.command("DST=%s" % dst) # echoed input dst
        if data != dst:
            raise weewx.WeeWxIOError("Failed to set DST to %s: %s" %
                                     (dst, data))
        data = self.read() # read ''
        if data not in ['ERROR', 'OK']:
            data = self.read() # read OK
        if data != 'OK':
            raise weewx.WeeWxIOError("Failed to set DST to %s: %s" %
                                     (dst, data))

    def get_units(self):
        log.debug("Get units")
        return self.command("UNITS=?")

    def set_units(self, units):
        log.debug("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, data))

    def get_interval(self):
        log.debug("Get logging interval")
        return int(self.command("LOGINT=?"))

    def set_interval(self, interval=5):
        log.debug("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" %
                                     data)

    def get_channel(self):
        log.debug("Get channel")
        return self.command("STATION")

    def set_channel(self, channel):
        log.debug("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" % data)

    def get_charger(self):
        log.debug("Get charger")
        return self.command("CHARGER")

    def get_baro(self):
        log.debug("Get baro")
        return self.command("BARO")

    def set_baro(self, offset):
        log.debug("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" % data)

    def get_memory_status(self):
        # query for logger memory use.  output is something like this:
        # 6438 bytes, 111 records, 0%
        log.debug("Get memory status")
        return self.command("MEM=?")

    def get_max(self):
        log.debug("Get max values")
        # Return outside temperature, humidity, pressure, wind direction,
        # wind speed, rainfall (daily total), station voltage, inside
        # temperature.
        return self.command("MAX=?").split(',')

    def reset_max(self):
        log.debug("Reset max values")
        data = self.command("MAX=RESET")
        if data != 'OK':
            raise weewx.WeeWxIOError("Failed to reset max values: %s" % data)

    def get_min(self):
        log.debug("Get min values")
        # Return outside temperature, humidity, pressure, wind direction,
        # wind speed, rainfall (ignore), station voltage, inside temperature.
        return self.command("MIN=?").split(',')

    def reset_min(self):
        log.debug("Reset min values")
        data = self.command("MIN=RESET")
        if data != 'OK':
            raise weewx.WeeWxIOError("Failed to reset min values: %s" % data)

    def get_history_usage(self):
        # return the number of records in the logger
        s = self.get_memory_status()
        if 'records' in s:
            return int(s.split(',')[1].split()[0])
        return None

    def clear_memory(self):
        log.debug("Clear memory")
        data = self.command("MEM=CLEAR")
        # It's a long wait for the OK.  With a greatly increased timeout
        # just for MEM=CLEAR, we should be able to read the OK.
        if data == 'OK':
            log.info("MEM=CLEAR succeeded.")
        else:
            raise weewx.WeeWxIOError("Failed to clear memory: %s" % data)

    def get_rain(self):
        log.debug("Get rain total")
        # Firmware 1.3 Build 022 Dec 02 2017 returns OK after the rain count
        # This is like TIME=?
        rstr = self.command("RAIN")
        if rstr not in ['ERROR', 'OK']:
            data = self.read()
        if data != 'OK':
            raise weewx.WeeWxIOError("Failed to get rain: %s" % data)
        return rstr

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

    def gen_records_since_ts(self, header, sensor_map, since_ts):
        if since_ts is None:
            since_ts = 0.0
            num_records = 0
        else:
            now_ts = time.mktime(datetime.datetime.now().timetuple())
            nseconds = now_ts - since_ts
            nminutes = math.ceil(nseconds / 60.0)
            num_records = math.ceil(nminutes / float(self.get_interval()))
        if num_records == 0:
            log.debug('gen_records_since_ts: Asking for all records.')
        else:
            log.debug('gen_records_since_ts: Asking for %d records.' % num_records)
        for r in self.gen_records(nrec=num_records):
            pkt = CC3000Driver._parse_values(r[1:], header, sensor_map, "%Y/%m/%d %H:%M")
            if 'dateTime' in pkt and pkt['dateTime'] > since_ts:
                yield pkt

    def gen_records(self, nrec=0):
        """
        Generator function for getting nrec records from the device.  A value
        of 0 indicates all records.

        The CC3000 returns a header ('HDR,'), the archive records
        we are interested in ('REC,'), daily max and min records
        ('MAX,', 'MIN,') as well as messages for various events such as a
        reboot ('MSG,').

        Things get interesting when nrec is non-zero.

        DOWNLOAD=n returns the latest n records in memory.  The CC3000 does
        not distinguish between REC, MAX, MIN and MSG records in memory.
        As such, DOWNLOAD=5 does NOT mean fetch the latest 5 REC records.
        For example, if the latest 5 records include a MIN and a MAX record,
        only 3 REC records will be returned (along with the MIN and MAX
        records).

        Given that one can't ask pecisely ask for a given number of archive
        records, a heuristic is used and errs on the side of asking for
        too many records.

        The heurisitic for number of records to ask for is:
        the sum of:
            nrec
            7 * the number of days convered in the request (rounded up)
            Note: One can determine the number of days from the number of
                  records requested because the archive interval is known.

        Asking for an extra seven records per day allows for the one MIN and
        one MAX records generated per day, plus a buffer for up to five MSG
        records each day.  Unless one is rebooting the CC3000 all day, this
        will be plenty.  Typically, there will be zero MSG records.  Clearing
        memory and rebooting actions generate MSG records.  Both are uncommon.
        As a result, gen_records will overshoot the records asked for, but this
        is not a problem in practice.  Also, if a new archive record is written
        while this operation is taking place, it will be returned.  As such,
        the number wouldn't be precise anyway.  One could work around this by
        accumulating records before returning, and then returning an exact
        amount, but it simply isn't worth it.

        Examining the records in the CC3000 (808 records at the time of the
        examination) shows the following records found:
            HDR: 1   (the header record, per the spec)
            REC: 800 (the archive records -- ~2.8 days worth)
            MSG: 1 (A clear command that executed ~2.8 days ago:
                    MSG 2019/12/20 15:48 CLEAR ON COMMAND!749D)
            MIN: 3 (As expected for 3 days.)
            MAX: 3 (As expected for 3 days.)

        Interrogating the CC3000 for a large number of records fails miserably
        if, while reading the responses, the responses are parsed and added
        to the datbase.  (Check sum mismatches, partical records, etc.).  If
        these last two steps are skipped, reading from the CC3000 is very
        reliable.  This can be observed by asing for history with wee_config.
        Observed with > 11K of records.

        To address the above problem, all records are read into memory.  Reading
        all records into memory before parsing and inserting into the database
        is very reliable.  For smaller amounts of recoreds, the reading into
        memory could be skipped, but what would be the point?
        """

        log.debug('gen_records(%d)' % nrec)
        totrec = self.get_history_usage()
        log.debug('gen_records: Requested %d latest of %d records.' % (nrec, totrec))

        if nrec == 0:
            num_to_ask = 0
        else:
            # Determine the number of records to ask for.
            # See heuristic above.
            num_mins_asked = nrec * self.get_interval()
            num_days_asked = math.ceil(num_mins_asked / (24.0*60))
            num_to_ask = nrec + 7 * num_days_asked

        if num_to_ask == 0:
            cmd = 'DOWNLOAD'
        else:
            cmd = 'DOWNLOAD=%d' % num_to_ask
        log.debug('%s' % cmd)

        # Note: It takes about 14s to read 1000 records into memory.
        if num_to_ask == 0:
            log.info('Reading all records into memory.  This could take some time.')
        elif num_to_ask < 1000:
            log.info('Reading %d records into memory.' % num_to_ask)
        else:
            log.info('Reading %d records into memory.  This could take some time.' % num_to_ask)
        yielded = 0
        recs = []
        data = self.command(cmd)
        while data != 'OK':
            recs.append(data)
            data = self.read()
        log.info('Finished reading %d records.' % len(recs))
        yielded = 0
        for data in recs:
            values = data.split(',')
            if values[0] == 'REC':
                yielded += 1
                yield values
            elif (values[0] == 'HDR' or values[0] == 'MSG' or
                  values[0] == 'MIN' or values[0] == 'MAX' or
                  values[0].startswith('DOWNLOAD')):
                pass
            else:
                log.error("Unexpected record '%s' (%s)" % (values[0], data))
        log.debug('Downloaded %d records' % yielded)

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.  invoke from the weewx root dir:
#
# PYTHONPATH=bin python -m weewx.drivers.cc3000 --help
#
# FIXME: This duplicates all of the functionality in CC3000Conigurator.
#        Perhaps pare this down to a version option and, by default,
#        polling and printing records (a la, the vantage driver)..

if __name__ == '__main__':
    import optparse

    import weewx
    import weeutil.logger

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

    parser = optparse.OptionParser(usage=usage)
    parser.add_option('--version', action='store_true',
                      help='display driver version')
    parser.add_option('--test-crc', dest='testcrc', action='store_true',
                      help='test crc')
    parser.add_option('--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('--debug', action='store_true', default=False,
                      help='emit additional diagnostic information')
    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('--get-rain', dest='getrain', action='store_true',
                      help='get rain counter')
    parser.add_option('--reset-rain', dest='resetrain', action='store_true',
                      help='reset rain counter')
    parser.add_option('--get-max', dest='getmax', action='store_true',
                      help='get max counter')
    parser.add_option('--reset-max', dest='resetmax', action='store_true',
                      help='reset max counters')
    parser.add_option('--get-min', dest='getmin', action='store_true',
                      help='get min counter')
    parser.add_option('--reset-min', dest='resetmin', action='store_true',
                      help='reset min counters')
    parser.add_option('--poll', metavar='POLL_INTERVAL', type=int,
                      help='poll interval in seconds')
    parser.add_option('--reboot', dest='reboot', action='store_true',
                      help='reboot the station')
    (options, args) = parser.parse_args()

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

    if options.debug:
        DEBUG_SERIAL = 1
        DEBUG_CHECKSUM = 1
        DEBUG_OPENCLOSE = 1
        weewx.debug = 1

    weeutil.logger.setup('cc3000', {})

    if options.testcrc:
        _check_crc(b'OK')
        _check_crc(b'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(b'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.reboot:
            print('rebooting...')
            startup_msgs = s.reboot()
            for line in startup_msgs:
                print(line)
        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())
            print("Max values:", s.get_max())
            print("Min values:", s.get_min())
        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.getrain:
            print(s.get_rain())
        if options.resetrain:
            print(s.reset_rain())
        if options.getmax:
            print(s.get_max())
        if options.resetmax:
            print(s.reset_max())
        if options.getmin:
            print(s.get_min())
        if options.resetmin:
            print(s.reset_min())
        if options.poll is not None:
            cmd_mode = True
            if options.poll == 0:
                cmd_mode = False
                s.set_auto()
            while True:
                print(s.get_current_data(cmd_mode))
                time.sleep(options.poll)
