#!/usr/bin/env python
#
# Copyright 2013-2015 Matthew Wall, Andrew Miles
# See the file LICENSE.txt for your full rights.
#
# Thanks to Andrew Miles for figuring out how to read history records
#   and many station parameters.
# Thanks to Sebastian John for the te923tool written in C (v0.6.1):
#   http://te923.fukz.org/
# Thanks to Mark Teel for the te923 implementation in wview:
#   http://www.wviewweather.com/
# Thanks to mrbalky:
#   https://github.com/mrbalky/te923/blob/master/README.md

"""Classes and functions for interfacing with te923 weather stations.

These stations were made by Hideki and branded as Honeywell, Meade, IROX Pro X,
Mebus TE923, and TFA Nexus.  They date back to at least 2007 and are still
sold (sparsely in the US, more commonly in Europe) as of 2013.

Apparently there are at least two different memory sizes.  One version can
store about 200 records, a newer version can store about 3300 records.

The firmware version of each component can be read by talking to the station,
assuming that the component has a wireless connection to the station, of
course.

To force connection between station and sensors, press and hold DOWN button.

To reset all station parameters:
 - press and hold SNOOZE and UP for 4 seconds
 - press SET button; main unit will beep
 - wait until beeping stops
 - remove batteries and wait 10 seconds
 - reinstall batteries

From the Meade TE9233W manual (TE923W-M_IM(ENG)_BK_010511.pdf):

  Remote temperature/humidty sampling interval: 10 seconds
  Remote temperature/humidity transmit interval: about 47 seconds
  Indoor temperature/humidity sampling interval: 10 seconds
  Indoor pressure sampling interval: 20 minutes
  Rain counter transmitting interval: 183 seconds
  Wind direction transmitting interval: 33 seconds
  Wind/Gust speed display update interval: 33 seconds
  Wind/Gust sampling interval: 11 seconds
  UV transmitting interval: 300 seconds
  Rain counter resolution: 0.03 in (0.6578 mm)
    (but console shows instead: 1/36 in (0.705556 mm))
  Battery status of each sensor is checked every hour

This implementation polls the station for data.  Use the polling_interval to
control the frequency of polling.  Default is 10 seconds.

The manual claims that a single bucket tip is 0.03 inches or 0.6578 mm but
neither matches the console display.  In reality, a single bucket tip is
between 0.02 and 0.03 in (0.508 to 0.762 mm).  This driver uses a value of 1/36
inch as observed in 36 bucket tips per 1.0 inches displayed on the console.
1/36 = 0.02777778 inch = 0.705555556 mm, or 1.0725989 times larger than the
0.02589 inch = 0.6578 mm that was used prior to version 0.41.1.

The station has altitude, latitude, longitude, and time.

Setting the time does not persist.  If you set the station time using weewx,
the station initially indicates that it is set to the new time, but then it
reverts.

Notes From/About Other Implementations

Apparently te923tool came first, then wview copied a bit from it.  te923tool
provides more detail about the reason for invalid values, for example, values
out of range versus no link with sensors.  However, these error states have not
yet been corroborated.

There are some disagreements between the wview and te923tool implementations.

From the te923tool:
- reading from usb in 8 byte chunks instead of all at once
- length of buffer is 35, but reads are 32-byte blocks
- windspeed and windgust state can never be -1
- index 29 in rain count, also in wind dir

From wview:
- wview does the 8-byte reads using interruptRead
- wview ignores the windchill value from the station
- wview treats the pressure reading as barometer (SLP), then calculates the
    station pressure and altimeter pressure

Memory Map

0x020000 - Last sample:

[00] = Month (Bits 0-3), Weekday (1 = Monday) (Bits 7:4)
[01] = Day
[02] = Hour
[03] = Minute
[04] ... reading as below

0x020001 - Current readings:

[00] = Temp In Low BCD
[01] = Temp In High BCD (Bit 5 = 0.05 deg, Bit 7 = -ve)
[02] = Humidity In
[03] = Temp Channel 1 Low (No link = Xa)
[04] = Temp Channel 1 High (Bit 6 = 1, Bit 5 = 0.05 deg, Bit 7 = +ve)
[05] = Humidity Channel 1 (No link = Xa)
[06] = Temp Channel 2 Low (No link = Xa)
[07] = Temp Channel 2 High (Bit 6 = 1, Bit 5 = 0.05 deg, Bit 7 = +ve)
[08] = Humidity Channel 2 (No link = Xa)
[09] = Temp Channel 3 Low (No link = Xa)
[10] = Temp Channel 3 High (Bit 6 = 1, Bit 5 = 0.05 deg, Bit 7 = +ve)
[11] = Humidity Channel 3 (No link = Xa)
[12] = Temp Channel 4 Low (No link = Xa)
[13] = Temp Channel 4 High (Bit 6 = 1, Bit 5 = 0.05 deg, Bit 7 = +ve)
[14] = Humidity Channel 4 (No link = Xa)
[15] = Temp Channel 5 Low (No link = Xa)
[16] = Temp Channel 5 High (Bit 6 = 1, Bit 5 = 0.05 deg, Bit 7 = +ve)
[17] = Humidity Channel 5 (No link = Xa)
[18] = UV Low (No link = ff)
[19] = UV High (No link = ff)
[20] = Sea-Level Pressure Low
[21] = Sea-Level Pressure High
[22] = Forecast (Bits 0-2) Storm (Bit 3)
[23] = Wind Chill Low (No link = ff)
[24] = Wind Chill High (Bit 6 = 1, Bit 5 = 0.05 deg, Bit 7 = +ve, No link = ff)
[25] = Gust Low (No link = ff)
[26] = Gust High (No link = ff)
[27] = Wind Low (No link = ff)
[28] = Wind High (No link = ff)
[29] = Wind Dir (Bits 0-3)
[30] = Rain Low
[31] = Rain High

(1) Memory map values related to sensors use same coding as above
(2) Checksum are via subtraction: 0x100 - sum of all values, then add 0x100
    until positive i.e. 0x100 - 0x70 - 0x80 - 0x28 = -0x18, 0x18 + 0x100 = 0xE8

SECTION 1: Date & Local location

0x000000 - Unknown - changes if date section is modified but still changes if
           same data is written so not a checksum
0x000001 - Unknown (always 0)
0x000002 - Day (Reverse BCD) (Changes at midday!)
0x000003 - Unknown
0x000004 - Year (Reverse BCD)
0x000005 - Month (Bits 7:4), Weekday (Bits 3:1)
0x000006 - Latitude (degrees) (reverse BCD)
0x000007 - Latitude (minutes) (reverse BCD)
0x000008 - Longitude (degrees) (reverse BCD)
0x000009 - Longitude (minutes) (reverse BCD)
0x00000A - Bit 7 - Set if Latitude southerly
           Bit 6 - Set if Longitude easterly
           Bit 4 - Set if DST is always on
           Bit 3 - Set if -ve TZ
           Bits 0 & 1 - Set if half-hour TZ
0x00000B - Longitude (100 degrees) (Bits 7:4), DST zone (Bits 3:0)
0x00000C - City code (High) (Bits 7:4)
           Language (Bits 3:0)
            0 - English
            1 - German
            2 - French
            3 - Italian
            4 - Spanish
            6 - Dutch
0x00000D - Timezone (hour) (Bits 7:4), City code (Low) (Bits 3:0)
0x00000E - Bit 2 - Set if 24hr time format
           Bit 1 - Set if 12hr time format
0x00000F - Checksum of 00:0E

SECTION 2: Time Alarms

0x000010 - Weekday alarm (hour) (reverse BCD)
           Bit 3 - Set if single alarm active
           Bit 2 - Set if weekday-alarm active
0x000011 - Weekday alarm (minute) (reverse BCD)
0x000012 - Single alarm (hour) (reverse BCD) (Bit 3 - Set if pre-alarm active)
0x000013 - Single alarm (minute) (reverse BCD)
0x000014 - Bits 7-4: Pre-alarm (1-5 = 15,30,45,60 or 90 mins)
           Bits 3-0: Snooze value
0x000015 - Checksum of 10:14

SECTION 3: Alternate Location

0x000016 - Latitude (degrees) (reverse BCD)
0x000017 - Latitude (minutes) (reverse BCD)
0x000018 - Longitude (degrees) (reverse BCD)
0x000019 - Longitude (minutes) (reverse BCD)
0x00001A - Bit 7 - Set if Latitude southerly
           Bit 6 - Set if Longitude easterly
           Bit 4 - Set if DST is always on
           Bit 3 - Set if -ve TZ
           Bits 0 & 1 - Set if half-hour TZ
0x00001B - Longitude (100 degrees) (Bits 7:4), DST zone (Bits 3:0)
0x00001C - City code (High) (Bits 7:4), Unknown (Bits 3:0)
0x00001D - Timezone (hour) (Bits 7:4), City code (Low) (Bits 3:0)
0x00001E - Checksum of 16:1D

SECTION 4: Temperature Alarms

0x00001F:20 - High Temp Alarm Value
0x000021:22 - Low Temp Alarm Value
0x000023 - Checksum of 1F:22

SECTION 5: Min/Max 1

0x000024:25 - Min In Temp
0x000026:27 - Max in Temp
0x000028 - Min In Humidity
0x000029 - Max In Humidity
0x00002A:2B - Min Channel 1 Temp
0x00002C:2D - Max Channel 1 Temp
0x00002E - Min Channel 1 Humidity
0x00002F - Max Channel 1 Humidity
0x000030:31 - Min Channel 2 Temp
0x000032:33 - Max Channel 2 Temp
0x000034 - Min Channel 2 Humidity
0x000035 - Max Channel 2 Humidity
0x000036:37 - Min Channel 3 Temp
0x000038:39 - Max Channel 3 Temp
0x00003A - Min Channel 3 Humidity
0x00003B - Max Channel 3 Humidity
0x00003C:3D - Min Channel 4 Temp
0x00003F - Checksum of 24:3E

SECTION 6: Min/Max 2

0x00003E,40 - Max Channel 4 Temp
0x000041 - Min Channel 4 Humidity
0x000042 - Max Channel 4 Humidity
0x000043:44 - Min Channel 4 Temp
0x000045:46 - Max Channel 4 Temp
0x000047 - Min Channel 4 Humidity
0x000048 - Max Channel 4 Humidity
0x000049 - ? Values rising/falling ?
           Bit 5 : Chan 1 temp falling
           Bit 2 : In temp falling
0x00004A:4B - 0xFF (Unused)
0x00004C - Battery status
           Bit 7: Rain
           Bit 6: Wind
           Bit 5: UV
           Bits 4:0: Channel 5:1
0x00004D:58 - 0xFF (Unused)
0x000059 - Checksum of 3E:58

SECTION 7: Altitude

0x00005A:5B - Altitude (Low:High)
0x00005C - Bit 3 - Set if altitude negative
           Bit 2 - Pressure falling?
           Bit 1 - Always set
0X00005D - Checksum of 5A:5C

0x00005E:5F - Unused (0xFF)

SECTION 8: Pressure 1

0x000060 - Month of last reading (Bits 0-3), Weekday (1 = Monday) (Bits 7:4)
0x000061 - Day of last reading
0x000062 - Hour of last reading
0x000063 - Minute of last reading
0x000064:65 - T -0 Hours
0x000066:67 - T -1 Hours
0x000068:69 - T -2 Hours
0x00006A:6B - T -3 Hours
0x00006C:6D - T -4 Hours
0x00006E:6F - T -5 Hours
0x000070:71 - T -6 Hours
0x000072:73 - T -7 Hours
0x000074:75 - T -8 Hours
0x000076:77 - T -9 Hours
0x000078:79 - T -10 Hours
0x00007B - Checksum of 60:7A

SECTION 9: Pressure 2

0x00007A,7C - T -11 Hours
0x00007D:7E - T -12 Hours
0x00007F:80 - T -13 Hours
0x000081:82 - T -14 Hours
0x000083:84 - T -15 Hours
0x000085:86 - T -16 Hours
0x000087:88 - T -17 Hours
0x000089:90 - T -18 Hours
0x00008B:8C - T -19 Hours
0x00008D:8E - T -20 Hours
0x00008f:90 - T -21 Hours
0x000091:92 - T -22 Hours
0x000093:94 - T -23 Hours
0x000095:96 - T -24 Hours
0x000097 - Checksum of 7C:96

SECTION 10: Versions

0x000098 - firmware versions (barometer)
0x000099 - firmware versions (uv)
0x00009A - firmware versions (rcc)
0x00009B - firmware versions (wind)
0x00009C - firmware versions (system)
0x00009D - Checksum of 98:9C

0x00009E:9F - 0xFF (Unused)

SECTION 11: Rain/Wind Alarms 1

0x0000A0 - Alarms
           Bit2 - Set if rain alarm active
           Bit 1 - Set if wind alarm active
           Bit 0 - Set if gust alarm active
0x0000A1:A2 - Rain alarm value (High:Low) (BCD)
0x0000A3 - Unknown
0x0000A4:A5 - Wind speed alarm value
0x0000A6 - Unknown
0x0000A7:A8 - Gust alarm value
0x0000A9 - Checksum of A0:A8

SECTION 12: Rain/Wind Alarms 2

0x0000AA:AB - Max daily wind speed
0x0000AC:AD - Max daily gust speed
0x0000AE:AF - Rain bucket count (yesterday) (Low:High)
0x0000B0:B1 - Rain bucket count (week) (Low:High)
0x0000B2:B3 - Rain bucket count (month) (Low:High)
0x0000B4 - Checksum of AA:B3

0x0000B5:E0 - 0xFF (Unused)

SECTION 13: Unknownn

0x0000E1:F9 - 0x15 (Unknown)
0x0000FA  - Checksum of E1:F9

SECTION 14: Archiving

0x0000FB - Unknown
0x0000FC - Memory size (0 = 0x1fff, 2 = 0x20000)
0x0000FD - Number of records (High)
0x0000FE - Archive interval 
           1-11 = 5, 10, 20, 30, 60, 90, 120, 180, 240, 360, 1440 mins
0x0000FF - Number of records (Low)
0x000100 - Checksum of FB:FF

0x000101 - Start of historical records:

[00] = Month (Bits 0-3), Weekday (1 = Monday) (Bits 7:4)
[01] = Day
[02] = Hour
[03] = Minute
[04] = Temp In Low BCD
[05] = Temp In High BCD (Bit 5 = 0.05 deg, Bit 7 = -ve)
[06] = Humidity In
[07] = Temp Channel 1 Low (No link = Xa)
[08] = Temp Channel 1 High (Bit 6 = 1, Bit 5 = 0.05 deg, Bit 7 = +ve)
[09] = Humidity Channel 1 (No link = Xa)
[10] = Temp Channel 2 Low (No link = Xa)
[11] = Temp Channel 2 High (Bit 6 = 1, Bit 5 = 0.05 deg, Bit 7 = +ve)
[12] = Humidity Channel 2 (No link = Xa)
[13] = Temp Channel 3 Low (No link = Xa)
[14] = Temp Channel 3 High (Bit 6 = 1, Bit 5 = 0.05 deg, Bit 7 = +ve)
[15] = Checksum of bytes 0:14
[16] = Humidity Channel 3 (No link = Xa)
[17] = Temp Channel 4 Low (No link = Xa)
[18] = Temp Channel 4 High (Bit 6 = 1, Bit 5 = 0.05 deg, Bit 7 = +ve)
[19] = Humidity Channel 4 (No link = Xa)
[20] = Temp Channel 5 Low (No link = Xa)
[21] = Temp Channel 5 High (Bit 6 = 1, Bit 5 = 0.05 deg, Bit 7 = +ve)
[22] = Humidity Channel 5 (No link = Xa)
[23] = UV Low (No link = ff)
[24] = UV High (No link = ff)
[25] = Sea-Level Pressure Low
[26] = Sea-Level Pressure High
[27] = Forecast (Bits 0-2) Storm (Bit 3)
[28] = Wind Chill Low (No link = ff)
[29] = Wind Chill High (Bit 6 = 1, Bit 5 = 0.05 deg, Bit 7 = +ve, No link = ee)
[30] = Gust Low (No link = ff)
[31] = Gust High (No link = ff)
[32] = Wind Low (No link = ff)
[33] = Wind High (No link = ff)
[34] = Wind Dir (Bits 0-3)
[35] = Rain Low
[36] = Rain High
[37] = Checksum of bytes 16:36

USB Protocol

The station shows up on the USB as a HID.  Control packet is 8 bytes.

Read from station:
 0x05 (Length)
 0xAF (Read)
 Addr (Bit 17:16), Addr (Bits 15:8), Addr (Bits 7:0), CRC, Unused, Unused

Read acknowledge:
 0x24 (Ack)
 0xAF (Read)
 Addr (Bit 17:16), Addr (Bits 15:8), Addr (Bits 7:0), CRC, Unused, Unused

Write to station:
 0x07 (Length)
 0xAE (Write)
 Addr (Bit 17:16), Addr (Bits 15:8), Addr (Bits 7:0), Data1, Data2, Data3
 ... Data continue with 3 more packets of length 7 then ...
 0x02 (Length), Data32, CRC, Unused, Unused, Unused, Unused, Unused, Unused

Reads returns 32 bytes.  Write expects 32 bytes as well, but address must be
aligned to a memory-map section start address and will only write to that
section.

Schema Additions

The station emits more sensor data than the default schema (wview schema) can
handle.  This driver includes a mapping between the sensor data and the wview
schema, plus additional fields.  To use the default mapping with the wview
schema, these are the additional fields that must be added to the schema:

          ('extraTemp4',           'REAL'),
          ('extraHumid3',          'REAL'),
          ('extraHumid4',          'REAL'),
          ('extraBatteryStatus1',  'REAL'),
          ('extraBatteryStatus2',  'REAL'),
          ('extraBatteryStatus3',  'REAL'),
          ('extraBatteryStatus4',  'REAL'),
          ('windLinkStatus',       'REAL'),
          ('rainLinkStatus',       'REAL'),
          ('uvLinkStatus',         'REAL'),
          ('outLinkStatus',        'REAL'),
          ('extraLinkStatus1',     'REAL'),
          ('extraLinkStatus2',     'REAL'),
          ('extraLinkStatus3',     'REAL'),
          ('extraLinkStatus4',     'REAL'),
          ('forecast',             'REAL'),
          ('storm',                'REAL'),
"""

# TODO: figure out how to read gauge pressure instead of slp
# TODO: figure out how to clear station memory
# TODO: add option to reset rain total

# FIXME: set-date and sync-date do not work - something reverts the clock
# FIXME: is there any way to get rid of the bad header byte on first read?

from __future__ import with_statement
from __future__ import absolute_import
from __future__ import print_function

import logging
import time
import usb

import weewx.drivers
import weewx.wxformulas
from weeutil.weeutil import timestamp_to_string

log = logging.getLogger(__name__)

DRIVER_NAME = 'TE923'
DRIVER_VERSION = '0.41.1'

def loader(config_dict, engine):  # @UnusedVariable
    return TE923Driver(**config_dict[DRIVER_NAME])

def configurator_loader(config_dict):  # @UnusedVariable
    return TE923Configurator()

def confeditor_loader():
    return TE923ConfEditor()

DEBUG_READ = 1
DEBUG_WRITE = 1
DEBUG_DECODE = 0

# map the station data to the default database schema, plus extensions
DEFAULT_MAP = {
    'windLinkStatus': 'link_wind',
    'windBatteryStatus': 'bat_wind',
    'rainLinkStatus': 'link_rain',
    'rainBatteryStatus': 'bat_rain',
    'uvLinkStatus': 'link_uv',
    'uvBatteryStatus': 'bat_uv',
    'inTemp': 't_in',
    'inHumidity': 'h_in',
    'outTemp': 't_1',
    'outHumidity': 'h_1',
    'outTempBatteryStatus': 'bat_1',
    'outLinkStatus': 'link_1',
    'extraTemp1': 't_2',
    'extraHumid1': 'h_2',
    'extraBatteryStatus1': 'bat_2',
    'extraLinkStatus1': 'link_2',
    'extraTemp2': 't_3',
    'extraHumid2': 'h_3',
    'extraBatteryStatus2': 'bat_3',
    'extraLinkStatus2': 'link_3',
    'extraTemp3': 't_4',
    'extraHumid3': 'h_4',
    'extraBatteryStatus3': 'bat_4',
    'extraLinkStatus3': 'link_4',
    'extraTemp4': 't_5',
    'extraHumid4': 'h_5',
    'extraBatteryStatus4': 'bat_5',
    'extraLinkStatus4': 'link_5'
}


class TE923ConfEditor(weewx.drivers.AbstractConfEditor):
    @property
    def default_stanza(self):
        return """
[TE923]
    # This section is for the Hideki TE923 series of weather stations.

    # The station model, e.g., 'Meade TE923W' or 'TFA Nexus'
    model = TE923

    # The driver to use:
    driver = weewx.drivers.te923

    # The default configuration associates the channel 1 sensor with outTemp
    # and outHumidity.  To change this, or to associate other channels with
    # specific columns in the database schema, use the following map.
    #[[sensor_map]]
%s
""" % "\n".join(["    #   %s = %s" % (x, DEFAULT_MAP[x]) for x in DEFAULT_MAP])


class TE923Configurator(weewx.drivers.AbstractConfigurator):
    LOCSTR = "CITY|USR,LONG_DEG,LONG_MIN,E|W,LAT_DEG,LAT_MIN,N|S,TZ,DST"
    ALMSTR = "WEEKDAY,SINGLE,PRE_ALARM,SNOOZE,MAXTEMP,MINTEMP,RAIN,WIND,GUST"

    idx_to_interval = {
        1: "5 min", 2: "10 min", 3: "20 min", 4: "30 min", 5: "60 min",
        6: "90 min", 7: "2 hour", 8: "3 hour", 9: "4 hour", 10: "6 hour",
        11: "1 day"}

    interval_to_idx = {
        "5m": 1, "10m": 2, "20m": 3, "30m": 4, "60m": 5, "90m": 6,
        "2h": 7, "3h": 8, "4h": 9, "6h": 10, "1d": 11}

    forecast_dict = {
        0: 'heavy snow',
        1: 'light snow',
        2: 'heavy rain',
        3: 'light rain',
        4: 'heavy clouds',
        5: 'light clouds',
        6: 'sunny',
    }

    dst_dict = {
        0: ["NO", 'None'],
        1: ["SA", 'Australian'],
        2: ["SB", 'Brazilian'],
        3: ["SC", 'Chilian'],
        4: ["SE", 'European'],
        5: ["SG", 'Eqyptian'],
        6: ["SI", 'Cuban'],
        7: ["SJ", 'Iraq and Syria'],
        8: ["SK", 'Irkutsk and Moscow'],
        9: ["SM", 'Uruguayan'],
       10: ["SN", 'Nambian'],
       11: ["SP", 'Paraguayan'],
       12: ["SQ", 'Iranian'],
       13: ["ST", 'Tasmanian'],
       14: ["SU", 'American'],
       15: ["SZ", 'New Zealand'],
    }
    
    city_dict = {
        0: ["ADD", 3, 0, 9, 1, "N", 38, 44, "E", "Addis Ababa, Ethiopia"],
        1: ["ADL", 9.5, 1, 34, 55, "S", 138, 36, "E", "Adelaide, Australia"],
        2: ["AKR", 2, 4, 39, 55, "N", 32, 55, "E", "Ankara, Turkey"],
        3: ["ALG", 1, 0, 36, 50, "N", 3, 0, "E", "Algiers, Algeria"],
        4: ["AMS", 1, 4, 52, 22, "N", 4, 53, "E", "Amsterdam, Netherlands"],
        5: ["ARN", 1, 4, 59, 17, "N", 18, 3, "E", "Stockholm Arlanda, Sweden"],
        6: ["ASU", -3, 11, 25, 15, "S", 57, 40, "W", "Asuncion, Paraguay"],
        7: ["ATH", 2, 4, 37, 58, "N", 23, 43, "E", "Athens, Greece"],
        8: ["ATL", -5, 14, 33, 45, "N", 84, 23, "W", "Atlanta, Ga."],
        9: ["AUS", -6, 14, 30, 16, "N", 97, 44, "W", "Austin, Tex."],
       10: ["BBU", 2, 4, 44, 25, "N", 26, 7, "E", "Bucharest, Romania"],
       11: ["BCN", 1, 4, 41, 23, "N", 2, 9, "E", "Barcelona, Spain"],
       12: ["BEG", 1, 4, 44, 52, "N", 20, 32, "E", "Belgrade, Yugoslavia"],
       13: ["BEJ", 8, 0, 39, 55, "N", 116, 25, "E", "Beijing, China"],
       14: ["BER", 1, 4, 52, 30, "N", 13, 25, "E", "Berlin, Germany"],
       15: ["BHM", -6, 14, 33, 30, "N", 86, 50, "W", "Birmingham, Ala."],
       16: ["BHX", 0, 4, 52, 25, "N", 1, 55, "W", "Birmingham, England"],
       17: ["BKK", 7, 0, 13, 45, "N", 100, 30, "E", "Bangkok, Thailand"],
       18: ["BNA", -6, 14, 36, 10, "N", 86, 47, "W", "Nashville, Tenn."],
       19: ["BNE", 10, 0, 27, 29, "S", 153, 8, "E", "Brisbane, Australia"],
       20: ["BOD", 1, 4, 44, 50, "N", 0, 31, "W", "Bordeaux, France"],
       21: ["BOG", -5, 0, 4, 32, "N", 74, 15, "W", "Bogota, Colombia"],
       22: ["BOS", -5, 14, 42, 21, "N", 71, 5, "W", "Boston, Mass."],
       23: ["BRE", 1, 4, 53, 5, "N", 8, 49, "E", "Bremen, Germany"],
       24: ["BRU", 1, 4, 50, 52, "N", 4, 22, "E", "Brussels, Belgium"],
       25: ["BUA", -3, 0, 34, 35, "S", 58, 22, "W", "Buenos Aires, Argentina"],
       26: ["BUD", 1, 4, 47, 30, "N", 19, 5, "E", "Budapest, Hungary"],
       27: ["BWI", -5, 14, 39, 18, "N", 76, 38, "W", "Baltimore, Md."],
       28: ["CAI", 2, 5, 30, 2, "N", 31, 21, "E", "Cairo, Egypt"],
       29: ["CCS", -4, 0, 10, 28, "N", 67, 2, "W", "Caracas, Venezuela"],
       30: ["CCU", 5.5, 0, 22, 34, "N", 88, 24, "E", "Calcutta, India (as Kolkata)"],
       31: ["CGX", -6, 14, 41, 50, "N", 87, 37, "W", "Chicago, IL"],
       32: ["CLE", -5, 14, 41, 28, "N", 81, 37, "W", "Cleveland, Ohio"],
       33: ["CMH", -5, 14, 40, 0, "N", 83, 1, "W", "Columbus, Ohio"],
       34: ["COR", -3, 0, 31, 28, "S", 64, 10, "W", "Cordoba, Argentina"],
       35: ["CPH", 1, 4, 55, 40, "N", 12, 34, "E", "Copenhagen, Denmark"],
       36: ["CPT", 2, 0, 33, 55, "S", 18, 22, "E", "Cape Town, South Africa"],
       37: ["CUU", -6, 14, 28, 37, "N", 106, 5, "W", "Chihuahua, Mexico"],
       38: ["CVG", -5, 14, 39, 8, "N", 84, 30, "W", "Cincinnati, Ohio"],
       39: ["DAL", -6, 14, 32, 46, "N", 96, 46, "W", "Dallas, Tex."],
       40: ["DCA", -5, 14, 38, 53, "N", 77, 2, "W", "Washington, D.C."],
       41: ["DEL", 5.5, 0, 28, 35, "N", 77, 12, "E", "New Delhi, India"],
       42: ["DEN", -7, 14, 39, 45, "N", 105, 0, "W", "Denver, Colo."],
       43: ["DKR", 0, 0, 14, 40, "N", 17, 28, "W", "Dakar, Senegal"],
       44: ["DTW", -5, 14, 42, 20, "N", 83, 3, "W", "Detroit, Mich."],
       45: ["DUB", 0, 4, 53, 20, "N", 6, 15, "W", "Dublin, Ireland"],
       46: ["DUR", 2, 0, 29, 53, "S", 30, 53, "E", "Durban, South Africa"],
       47: ["ELP", -7, 14, 31, 46, "N", 106, 29, "W", "El Paso, Tex."],
       48: ["FIH", 1, 0, 4, 18, "S", 15, 17, "E", "Kinshasa, Congo"],
       49: ["FRA", 1, 4, 50, 7, "N", 8, 41, "E", "Frankfurt, Germany"],
       50: ["GLA", 0, 4, 55, 50, "N", 4, 15, "W", "Glasgow, Scotland"],
       51: ["GUA", -6, 0, 14, 37, "N", 90, 31, "W", "Guatemala City, Guatemala"],
       52: ["HAM", 1, 4, 53, 33, "N", 10, 2, "E", "Hamburg, Germany"],
       53: ["HAV", -5, 6, 23, 8, "N", 82, 23, "W", "Havana, Cuba"],
       54: ["HEL", 2, 4, 60, 10, "N", 25, 0, "E", "Helsinki, Finland"],
       55: ["HKG", 8, 0, 22, 20, "N", 114, 11, "E", "Hong Kong, China"],
       56: ["HOU", -6, 14, 29, 45, "N", 95, 21, "W", "Houston, Tex."],
       57: ["IKT", 8, 8, 52, 30, "N", 104, 20, "E", "Irkutsk, Russia"],
       58: ["IND", -5, 0, 39, 46, "N", 86, 10, "W", "Indianapolis, Ind."],
       59: ["JAX", -5, 14, 30, 22, "N", 81, 40, "W", "Jacksonville, Fla."],
       60: ["JKT", 7, 0, 6, 16, "S", 106, 48, "E", "Jakarta, Indonesia"],
       61: ["JNB", 2, 0, 26, 12, "S", 28, 4, "E", "Johannesburg, South Africa"],
       62: ["KIN", -5, 0, 17, 59, "N", 76, 49, "W", "Kingston, Jamaica"],
       63: ["KIX", 9, 0, 34, 32, "N", 135, 30, "E", "Osaka, Japan"],
       64: ["KUL", 8, 0, 3, 8, "N", 101, 42, "E", "Kuala Lumpur, Malaysia"],
       65: ["LAS", -8, 14, 36, 10, "N", 115, 12, "W", "Las Vegas, Nev."],
       66: ["LAX", -8, 14, 34, 3, "N", 118, 15, "W", "Los Angeles, Calif."],
       67: ["LIM", -5, 0, 12, 0, "S", 77, 2, "W", "Lima, Peru"],
       68: ["LIS", 0, 4, 38, 44, "N", 9, 9, "W", "Lisbon, Portugal"],
       69: ["LON", 0, 4, 51, 32, "N", 0, 5, "W", "London, England"],
       70: ["LPB", -4, 0, 16, 27, "S", 68, 22, "W", "La Paz, Bolivia"],
       71: ["LPL", 0, 4, 53, 25, "N", 3, 0, "W", "Liverpool, England"],
       72: ["LYO", 1, 4, 45, 45, "N", 4, 50, "E", "Lyon, France"],
       73: ["MAD", 1, 4, 40, 26, "N", 3, 42, "W", "Madrid, Spain"],
       74: ["MEL", 10, 1, 37, 47, "S", 144, 58, "E", "Melbourne, Australia"],
       75: ["MEM", -6, 14, 35, 9, "N", 90, 3, "W", "Memphis, Tenn."],
       76: ["MEX", -6, 14, 19, 26, "N", 99, 7, "W", "Mexico City, Mexico"],
       77: ["MIA", -5, 14, 25, 46, "N", 80, 12, "W", "Miami, Fla."],
       78: ["MIL", 1, 4, 45, 27, "N", 9, 10, "E", "Milan, Italy"],
       79: ["MKE", -6, 14, 43, 2, "N", 87, 55, "W", "Milwaukee, Wis."],
       80: ["MNL", 8, 0, 14, 35, "N", 120, 57, "E", "Manila, Philippines"],
       81: ["MOW", 3, 8, 55, 45, "N", 37, 36, "E", "Moscow, Russia"],
       82: ["MRS", 1, 4, 43, 20, "N", 5, 20, "E", "Marseille, France"],
       83: ["MSP", -6, 14, 44, 59, "N", 93, 14, "W", "Minneapolis, Minn."],
       84: ["MSY", -6, 14, 29, 57, "N", 90, 4, "W", "New Orleans, La."],
       85: ["MUC", 1, 4, 48, 8, "N", 11, 35, "E", "Munich, Germany"],
       86: ["MVD", -3, 9, 34, 53, "S", 56, 10, "W", "Montevideo, Uruguay"],
       87: ["NAP", 1, 4, 40, 50, "N", 14, 15, "E", "Naples, Italy"],
       88: ["NBO", 3, 0, 1, 25, "S", 36, 55, "E", "Nairobi, Kenya"],
       89: ["NKG", 8, 0, 32, 3, "N", 118, 53, "E", "Nanjing (Nanking), China"],
       90: ["NYC", -5, 14, 40, 47, "N", 73, 58, "W", "New York, N.Y."],
       91: ["ODS", 2, 4, 46, 27, "N", 30, 48, "E", "Odessa, Ukraine"],
       92: ["OKC", -6, 14, 35, 26, "N", 97, 28, "W", "Oklahoma City, Okla."],
       93: ["OMA", -6, 14, 41, 15, "N", 95, 56, "W", "Omaha, Neb."],
       94: ["OSL", 1, 4, 59, 57, "N", 10, 42, "E", "Oslo, Norway"],
       95: ["PAR", 1, 4, 48, 48, "N", 2, 20, "E", "Paris, France"],
       96: ["PDX", -8, 14, 45, 31, "N", 122, 41, "W", "Portland, Ore."],
       97: ["PER", 8, 0, 31, 57, "S", 115, 52, "E", "Perth, Australia"],
       98: ["PHL", -5, 14, 39, 57, "N", 75, 10, "W", "Philadelphia, Pa."],
       99: ["PHX", -7, 0, 33, 29, "N", 112, 4, "W", "Phoenix, Ariz."],
      100: ["PIT", -5, 14, 40, 27, "N", 79, 57, "W", "Pittsburgh, Pa."],
      101: ["PRG", 1, 4, 50, 5, "N", 14, 26, "E", "Prague, Czech Republic"],
      102: ["PTY", -5, 0, 8, 58, "N", 79, 32, "W", "Panama City, Panama"],
      103: ["RGN", 6.5, 0, 16, 50, "N", 96, 0, "E", "Rangoon, Myanmar"],
      104: ["RIO", -3, 2, 22, 57, "S", 43, 12, "W", "Rio de Janeiro, Brazil"],
      105: ["RKV", 0, 0, 64, 4, "N", 21, 58, "W", "Reykjavik, Iceland"],
      106: ["ROM", 1, 4, 41, 54, "N", 12, 27, "E", "Rome, Italy"],
      107: ["SAN", -8, 14, 32, 42, "N", 117, 10, "W", "San Diego, Calif."],
      108: ["SAT", -6, 14, 29, 23, "N", 98, 33, "W", "San Antonio, Tex."],
      109: ["SCL", -4, 3, 33, 28, "S", 70, 45, "W", "Santiago, Chile"],
      110: ["SEA", -8, 14, 47, 37, "N", 122, 20, "W", "Seattle, Wash."],
      111: ["SFO", -8, 14, 37, 47, "N", 122, 26, "W", "San Francisco, Calif."],
      112: ["SHA", 8, 0, 31, 10, "N", 121, 28, "E", "Shanghai, China"],
      113: ["SIN", 8, 0, 1, 14, "N", 103, 55, "E", "Singapore, Singapore"],
      114: ["SJC", -8, 14, 37, 20, "N", 121, 53, "W", "San Jose, Calif."],
      115: ["SOF", 2, 4, 42, 40, "N", 23, 20, "E", "Sofia, Bulgaria"],
      116: ["SPL", -3, 2, 23, 31, "S", 46, 31, "W", "Sao Paulo, Brazil"],
      117: ["SSA", -3, 0, 12, 56, "S", 38, 27, "W", "Salvador, Brazil"],
      118: ["STL", -6, 14, 38, 35, "N", 90, 12, "W", "St. Louis, Mo."],
      119: ["SYD", 10, 1, 34, 0, "S", 151, 0, "E", "Sydney, Australia"],
      120: ["TKO", 9, 0, 35, 40, "N", 139, 45, "E", "Tokyo, Japan"],
      121: ["TPA", -5, 14, 27, 57, "N", 82, 27, "W", "Tampa, Fla."],
      122: ["TRP", 2, 0, 32, 57, "N", 13, 12, "E", "Tripoli, Libya"],
      123: ["USR", 0, 0, 0, 0, "N", 0, 0, "W", "User defined city"],
      124: ["VAC", -8, 14, 49, 16, "N", 123, 7, "W", "Vancouver, Canada"],
      125: ["VIE", 1, 4, 48, 14, "N", 16, 20, "E", "Vienna, Austria"],
      126: ["WAW", 1, 4, 52, 14, "N", 21, 0, "E", "Warsaw, Poland"],
      127: ["YMX", -5, 14, 45, 30, "N", 73, 35, "W", "Montreal, Que., Can."],
      128: ["YOW", -5, 14, 45, 24, "N", 75, 43, "W", "Ottawa, Ont., Can."],
      129: ["YTZ", -5, 14, 43, 40, "N", 79, 24, "W", "Toronto, Ont., Can."],
      130: ["YVR", -8, 14, 49, 13, "N", 123, 6, "W", "Vancouver, B.C., Can."],
      131: ["YYC", -7, 14, 51, 1, "N", 114, 1, "W", "Calgary, Alba., Can."],
      132: ["ZRH", 1, 4, 47, 21, "N", 8, 31, "E", "Zurich, Switzerland"]
    }
      
    @property
    def version(self):
        return DRIVER_VERSION

    def add_options(self, parser):
        super(TE923Configurator, 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="get the current weather conditions")
        parser.add_option("--history", dest="nrecords", type=int, metavar="N",
                          help="display N history records")
        parser.add_option("--history-since", dest="recmin",
                          type=int, metavar="N",
                          help="display history records since N minutes ago")
        parser.add_option("--minmax", dest="minmax", action="store_true",
                          help="display historical min/max data")
        parser.add_option("--get-date", dest="getdate", action="store_true",
                          help="display station date")
        parser.add_option("--set-date", dest="setdate",
                          type=str, metavar="YEAR,MONTH,DAY",
                          help="set station date")
        parser.add_option("--sync-date", dest="syncdate", action="store_true",
                          help="set station date using system clock")
        parser.add_option("--get-location-local", dest="loc_local",
                          action="store_true",
                          help="display local location and timezone")
        parser.add_option("--set-location-local", dest="setloc_local",
                          type=str, metavar=self.LOCSTR,
                          help="set local location and timezone")
        parser.add_option("--get-location-alt", dest="loc_alt",
                          action="store_true",
                          help="display alternate location and timezone")
        parser.add_option("--set-location-alt", dest="setloc_alt",
                          type=str, metavar=self.LOCSTR,
                          help="set alternate location and timezone")
        parser.add_option("--get-altitude", dest="getalt", action="store_true",
                          help="display altitude")
        parser.add_option("--set-altitude", dest="setalt", type=int,
                          metavar="ALT", help="set altitude (meters)")
        parser.add_option("--get-alarms", dest="getalarms",
                          action="store_true", help="display alarms")
        parser.add_option("--set-alarms", dest="setalarms", type=str,
                          metavar=self.ALMSTR, help="set alarm state")
        parser.add_option("--get-interval", dest="getinterval",
                          action="store_true", help="display archive interval")
        parser.add_option("--set-interval", dest="setinterval",
                          type=str, metavar="INTERVAL",
                          help="set archive interval (minutes)")
        parser.add_option("--format", dest="format",
                          type=str, metavar="FORMAT", default='table',
                          help="formats include: table, dict")

    def do_options(self, options, parser, config_dict, prompt):  # @UnusedVariable
        if (options.format.lower() != 'table' and
            options.format.lower() != 'dict'):
            parser.error("Unknown format '%s'.  Known formats include 'table' and 'dict'." % options.format)

        with TE923Station() as station:
            if options.info is not None:
                self.show_info(station, fmt=options.format)
            elif options.current is not None:
                self.show_current(station, fmt=options.format)
            elif options.nrecords is not None:
                self.show_history(station, count=options.nrecords,
                                  fmt=options.format)
            elif options.recmin is not None:
                ts = int(time.time()) - options.recmin * 60
                self.show_history(station, ts=ts, fmt=options.format)
            elif options.minmax is not None:
                self.show_minmax(station)
            elif options.getdate is not None:
                self.show_date(station)
            elif options.setdate is not None:
                self.set_date(station, options.setdate)
            elif options.syncdate:
                self.set_date(station, None)
            elif options.loc_local is not None:
                self.show_location(station, 0)
            elif options.setloc_local is not None:
                self.set_location(station, 0, options.setloc_local)
            elif options.loc_alt is not None:
                self.show_location(station, 1)
            elif options.setloc_alt is not None:
                self.set_location(station, 1, options.setloc_alt)
            elif options.getalt is not None:
                self.show_altitude(station)
            elif options.setalt is not None:
                self.set_altitude(station, options.setalt)
            elif options.getalarms is not None:
                self.show_alarms(station)
            elif options.setalarms is not None:
                self.set_alarms(station, options.setalarms)
            elif options.getinterval is not None:
                self.show_interval(station)
            elif options.setinterval is not None:
                self.set_interval(station, options.setinterval)

    @staticmethod
    def show_info(station, fmt='dict'):
        print('Querying the station for the configuration...')
        data = station.get_config()
        TE923Configurator.print_data(data, fmt)

    @staticmethod
    def show_current(station, fmt='dict'):
        print('Querying the station for current weather data...')
        data = station.get_readings()
        TE923Configurator.print_data(data, fmt)

    @staticmethod
    def show_history(station, ts=0, count=None, fmt='dict'):
        print("Querying the station for historical records...")
        for r in station.gen_records(ts, count):
            TE923Configurator.print_data(r, fmt)

    @staticmethod
    def show_minmax(station):
        print("Querying the station for historical min/max data")
        data = station.get_minmax()
        print("Console Temperature Min : %s" % data['t_in_min'])
        print("Console Temperature Max : %s" % data['t_in_max'])
        print("Console Humidity Min    : %s" % data['h_in_min'])
        print("Console Humidity Max    : %s" % data['h_in_max'])
        for i in range(1, 6):
            print("Channel %d Temperature Min : %s" % (i, data['t_%d_min' % i]))
            print("Channel %d Temperature Max : %s" % (i, data['t_%d_max' % i]))
            print("Channel %d Humidity Min    : %s" % (i, data['h_%d_min' % i]))
            print("Channel %d Humidity Max    : %s" % (i, data['h_%d_max' % i]))
        print("Wind speed max since midnight : %s" % data['windspeed_max'])
        print("Wind gust max since midnight  : %s" % data['windgust_max'])
        print("Rain yesterday  : %s" % data['rain_yesterday'])
        print("Rain this week  : %s" % data['rain_week'])
        print("Rain this month : %s" % data['rain_month'])
        print("Last Barometer reading : %s" % time.strftime(
            "%Y %b %d %H:%M", time.localtime(data['barometer_ts'])))
        for i in range(25):
            print("   T-%02d Hours : %.1f" % (i, data['barometer_%d' % i]))

    @staticmethod
    def show_date(station):
        ts = station.get_date()
        tt = time.localtime(ts)
        print("Date: %02d/%02d/%d" % (tt[2], tt[1], tt[0]))
        TE923Configurator.print_alignment()

    @staticmethod
    def set_date(station, datestr):
        if datestr is not None:
            date_list = datestr.split(',')
            if len(date_list) != 3:
                print("Bad date '%s', format is YEAR,MONTH,DAY" % datestr)
                return
            if int(date_list[0]) < 2000 or int(date_list[0]) > 2099:
                print("Year must be between 2000 and 2099 inclusive")
                return
            if int(date_list[1]) < 1 or int(date_list[1]) > 12:
                print("Month must be between 1 and 12 inclusive")
                return
            if int(date_list[2]) < 1 or int(date_list[2]) > 31:
                print("Day must be between 1 and 31 inclusive")
                return
            tt = time.localtime()
            offset = 1 if tt[3] < 12 else 0
            ts = time.mktime((int(date_list[0]), int(date_list[1]), int(date_list[2]) - offset, 0, 0, 0, 0, 0, 0))
        else:
            ts = time.time()
        station.set_date(ts)
        TE923Configurator.print_alignment()

    def show_location(self, station, loc_type):
        data = station.get_loc(loc_type)
        print("City     : %s (%s)" % (self.city_dict[data['city_time']][9],
                                      self.city_dict[data['city_time']][0]))
        degree_sign= u'\N{DEGREE SIGN}'.encode('iso-8859-1')
        print("Location : %03d%s%02d'%s %02d%s%02d'%s" % (
            data['long_deg'], degree_sign, data['long_min'], data['long_dir'],
            data['lat_deg'], degree_sign, data['lat_min'], data['lat_dir']))
        if data['dst_always_on']:
            print("DST      : Always on")
        else:
            print("DST      : %s (%s)" % (self.dst_dict[data['dst']][1],
                                          self.dst_dict[data['dst']][0]))

    def set_location(self, station, loc_type, location):
        dst_on = 1
        dst_index = 0
        location_list = location.split(',')
        if len(location_list) == 1 and location_list[0] != "USR":
            city_index = None
            for idx in range(len(self.city_dict)):
                if self.city_dict[idx][0] == location_list[0]:
                    city_index = idx
                    break
            if city_index is None:
                print("City code '%s' not recognized - consult station manual for valid city codes" % location_list[0])
                return
            long_deg = self.city_dict[city_index][6]
            long_min = self.city_dict[city_index][7]
            long_dir = self.city_dict[city_index][8]
            lat_deg = self.city_dict[city_index][3]
            lat_min = self.city_dict[city_index][4]
            lat_dir = self.city_dict[city_index][5]
            tz_hr = int(self.city_dict[city_index][1])
            tz_min = 0 if self.city_dict[city_index][1] == int(self.city_dict[city_index][1]) else 30
            dst_on = 0
            dst_index = self.city_dict[city_index][2]
        elif len(location_list) == 9 and location_list[0] == "USR":
            if int(location_list[1]) < 0 or int(location_list[1]) > 180:
                print("Longitude degrees must be between 0 and 180 inclusive")
                return
            if int(location_list[2]) < 0 or int(location_list[2]) > 180:
                print("Longitude minutes must be between 0 and 59 inclusive")
                return
            if location_list[3] != "E" and location_list[3] != "W":
                print("Longitude direction must be E or W")
                return
            if int(location_list[4]) < 0 or int(location_list[4]) > 180:
                print("Latitude degrees must be between 0 and 90 inclusive")
                return
            if int(location_list[5]) < 0 or int(location_list[5]) > 180:
                print("Latitude minutes must be between 0 and 59 inclusive")
                return
            if location_list[6] != "N" and location_list[6] != "S":
                print("Longitude direction must be N or S")
                return
            tz_list = location_list[7].split(':')
            if len(tz_list) != 2:
                print("Bad timezone '%s', format is HOUR:MINUTE" % location_list[7])
                return
            if int(tz_list[0]) < -12 or int(tz_list[0]) > 12:
                print("Timezone hour must be between -12 and 12 inclusive")
                return
            if int(tz_list[1]) != 0 and int(tz_list[1]) != 30:
                print("Timezone minute must be 0 or 30")
                return
            if location_list[8].lower() != 'on':
                dst_on = 0
                dst_index = None
                for idx in range(16):
                    if self.dst_dict[idx][0] == location_list[8]:
                        dst_index = idx
                        break
                if dst_index is None:
                    print("DST code '%s' not recognized - consult station manual for valid DST codes" % location_list[8])
                    return
            else:
                dst_on = 1
                dst_index = 0
            city_index = 123 # user-defined city
            long_deg = int(location_list[1])
            long_min = int(location_list[2])
            long_dir = location_list[3]
            lat_deg = int(location_list[4])
            lat_min = int(location_list[5])
            lat_dir = location_list[6]
            tz_hr = int(tz_list[0])
            tz_min = int(tz_list[1])
        else:
            print("Bad location '%s'" % location)
            print("Location format is: %s" % self.LOCSTR)
            return
        station.set_loc(loc_type, city_index, dst_on, dst_index, tz_hr, tz_min,
                        lat_deg, lat_min, lat_dir,
                        long_deg, long_min, long_dir)

    @staticmethod
    def show_altitude(station):
        altitude = station.get_alt()
        print("Altitude: %d meters" % altitude)

    @staticmethod
    def set_altitude(station, altitude):
        if altitude < -200 or altitude > 5000:
            print("Altitude must be between -200 and 5000 inclusive")
            return
        station.set_alt(altitude)

    @staticmethod
    def show_alarms(station):
        data = station.get_alarms()
        print("Weekday alarm : %02d:%02d (%s)" % (
            data['weekday_hour'], data['weekday_min'], data['weekday_active']))
        print("Single  alarm : %02d:%02d (%s)" % (
            data['single_hour'], data['single_min'], data['single_active']))
        print("Pre-alarm     : %s (%s)" % (
            data['prealarm_period'], data['prealarm_active']))
        if data['snooze'] > 0:
            print("Snooze        : %d mins" % data['snooze'])
        else:
            print("Snooze        : Invalid")
        print("Max Temperature Alarm : %s" % data['max_temp'])
        print("Min Temperature Alarm : %s" % data['min_temp'])
        print("Rain Alarm            : %d mm (%s)" % (
            data['rain'], data['rain_active']))
        print("Wind Speed Alarm      : %s (%s)" % (
            data['windspeed'], data['windspeed_active']))
        print("Wind Gust  Alarm      : %s (%s)" % (
            data['windgust'], data['windgust_active']))

    @staticmethod
    def set_alarms(station, alarm):
        alarm_list = alarm.split(',')
        if len(alarm_list) != 9:
            print("Bad alarm '%s'" % alarm)
            print("Alarm format is: %s" % TE923Configurator.ALMSTR)
            return
        weekday = alarm_list[0]
        if weekday.lower() != 'off':
            weekday_list = weekday.split(':')
            if len(weekday_list) != 2:
                print("Bad alarm '%s', expected HOUR:MINUTE or OFF" % weekday)
                return
            if int(weekday_list[0]) < 0 or int(weekday_list[0]) > 23:
                print("Alarm hours must be between 0 and 23 inclusive")
                return
            if int(weekday_list[1]) < 0 or int(weekday_list[1]) > 59:
                print("Alarm minutes must be between 0 and 59 inclusive")
                return
        single = alarm_list[1]
        if single.lower() != 'off':
            single_list = single.split(':')
            if len(single_list) != 2:
                print("Bad alarm '%s', expected HOUR:MINUTE or OFF" % single)
                return
            if int(single_list[0]) < 0 or int(single_list[0]) > 23:
                print("Alarm hours must be between 0 and 23 inclusive")
                return
            if int(single_list[1]) < 0 or int(single_list[1]) > 59:
                print("Alarm minutes must be between 0 and 59 inclusive")
                return
        if alarm_list[2].lower() != 'off' and alarm_list[2] not in ['15', '30', '45', '60', '90']:
            print("Prealarm must be 15, 30, 45, 60, 90 or OFF")
            return
        if int(alarm_list[3]) < 1 or int(alarm_list[3]) > 15:
            print("Snooze must be between 1 and 15 inclusive")
            return
        if float(alarm_list[4]) < -50 or float(alarm_list[4]) > 70:
            print("Temperature alarm must be between -50 and 70 inclusive")
            return
        if float(alarm_list[5]) < -50 or float(alarm_list[5]) > 70:
            print("Temperature alarm must be between -50 and 70 inclusive")
            return
        if alarm_list[6].lower() != 'off' and (int(alarm_list[6]) < 1 or int(alarm_list[6]) > 9999):
            print("Rain alarm must be between 1 and 999 inclusive or OFF")
            return
        if alarm_list[7].lower() != 'off' and (float(alarm_list[7]) < 1 or float(alarm_list[7]) > 199):
            print("Wind alarm must be between 1 and 199 inclusive or OFF")
            return
        if alarm_list[8].lower() != 'off' and (float(alarm_list[8]) < 1 or float(alarm_list[8]) > 199):
            print("Wind alarm must be between 1 and 199 inclusive or OFF")
            return
        station.set_alarms(alarm_list[0], alarm_list[1], alarm_list[2],
                           alarm_list[3], alarm_list[4], alarm_list[5],
                           alarm_list[6], alarm_list[7], alarm_list[8])
        print("Temperature alarms can only be modified via station controls")

    @staticmethod
    def show_interval(station):
        idx = station.get_interval()
        print("Interval: %s" % TE923Configurator.idx_to_interval.get(idx, 'unknown'))

    @staticmethod
    def set_interval(station, interval):
        """accept 30s|2h|1d format or raw minutes, but only known intervals"""
        idx = TE923Configurator.interval_to_idx.get(interval)
        if idx is None:
            try:
                ival = int(interval * 60)
                for i in TE923Station.idx_to_interval_sec:
                    if ival == TE923Station.idx_to_interval_sec[i]:
                        idx = i
            except ValueError:
                pass
        if idx is None:
            print("Bad interval '%s'" % interval)
            print("Valid intervals are %s" % ','.join(list(TE923Configurator.interval_to_idx.keys())))
            return
        station.set_interval(idx)

    @staticmethod
    def print_data(data, fmt):
        if fmt.lower() == 'table':
            TE923Configurator.print_table(data)
        else:
            print(data)

    @staticmethod
    def print_table(data):
        for key in sorted(data):
            print("%s: %s" % (key.rjust(16), data[key]))

    @staticmethod
    def print_alignment():
        print("  If computer time is not aligned to station time then date")
        print("  may be incorrect by 1 day")


class TE923Driver(weewx.drivers.AbstractDevice):
    """Driver for Hideki TE923 stations."""
    
    def __init__(self, **stn_dict):
        """Initialize the station object.

        polling_interval: How often to poll the station, in seconds.
        [Optional. Default is 10]

        model: Which station model is this?
        [Optional. Default is 'TE923']
        """
        log.info('driver version is %s' % DRIVER_VERSION)

        global DEBUG_READ
        DEBUG_READ = int(stn_dict.get('debug_read', DEBUG_READ))
        global DEBUG_WRITE
        DEBUG_WRITE = int(stn_dict.get('debug_write', DEBUG_WRITE))
        global DEBUG_DECODE
        DEBUG_DECODE = int(stn_dict.get('debug_decode', DEBUG_DECODE))

        self._last_rain_loop = None
        self._last_rain_archive = None
        self._last_ts = None

        self.model = stn_dict.get('model', 'TE923')
        self.max_tries = int(stn_dict.get('max_tries', 5))
        self.retry_wait = int(stn_dict.get('retry_wait', 3))
        self.read_timeout = int(stn_dict.get('read_timeout', 10))
        self.polling_interval = int(stn_dict.get('polling_interval', 10))
        log.info('polling interval is %s' % str(self.polling_interval))
        self.sensor_map = dict(DEFAULT_MAP)
        if 'sensor_map' in stn_dict:
            self.sensor_map.update(stn_dict['sensor_map'])
        log.info('sensor map is %s' % self.sensor_map)

        self.station = TE923Station(max_tries=self.max_tries,
                                    retry_wait=self.retry_wait,
                                    read_timeout=self.read_timeout)
        self.station.open()
        log.info('logger capacity %s records' % self.station.get_memory_size())
        ts = self.station.get_date()
        now = int(time.time())
        log.info('station time is %s, computer time is %s' % (ts, now))

    def closePort(self):
        if self.station is not None:
            self.station.close()
            self.station = None

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

#    @property
#    def archive_interval(self):
#        return self.station.get_interval_seconds()

    def genLoopPackets(self):
        while True:
            data = self.station.get_readings()
            status = self.station.get_status()
            packet = self.data_to_packet(data, status=status,
                                         last_rain=self._last_rain_loop,
                                         sensor_map=self.sensor_map)
            self._last_rain_loop = packet['rainTotal']
            yield packet
            time.sleep(self.polling_interval)

    # same as genStartupRecords, but insert battery status on the last record.
    # when record_generation is hardware, this results in a full suit of sensor
    # data, but with the archive interval calculations done by the hardware.
#    def genArchiveRecords(self, since_ts=0):
#        for data in self.station.gen_records(since_ts):
#            # FIXME: insert battery status on the last record
#            packet = self.data_to_packet(data, status=None,
#                                         last_rain=self._last_rain_archive,
#                                         sensor_map=self.sensor_map)
#            self._last_rain_archive = packet['rainTotal']
#            if self._last_ts:
#                packet['interval'] = (packet['dateTime'] - self._last_ts) / 60
#                yield packet
#            self._last_ts = packet['dateTime']

    # there is no battery status for historical records.
    def genStartupRecords(self, since_ts=0):
        log.info("reading records from logger since %s" % since_ts)
        cnt = 0
        for data in self.station.gen_records(since_ts):
            packet = self.data_to_packet(data, status=None,
                                         last_rain=self._last_rain_archive,
                                         sensor_map=self.sensor_map)
            self._last_rain_archive = packet['rainTotal']
            if self._last_ts:
                packet['interval'] = (packet['dateTime'] - self._last_ts) // 60
                if packet['interval'] > 0:
                    cnt += 1
                    yield packet
                else:
                    log.info("skip packet with duplidate timestamp: %s" % packet)
            self._last_ts = packet['dateTime']
            if cnt % 50 == 0:
                log.info("read %s records from logger" % cnt)
        log.info("read %s records from logger" % cnt)

    @staticmethod
    def data_to_packet(data, status, last_rain, sensor_map):
        """convert raw data to format and units required by weewx

                    station      weewx (metric)
    temperature     degree C     degree C
    humidity        percent      percent
    uv index        unitless     unitless
    slp             mbar         mbar
    wind speed      mile/h       km/h
    wind gust       mile/h       km/h
    wind dir        degree       degree
    rain            mm           cm
    rain rate                    cm/h
    """

        packet = dict()
        packet['usUnits'] = weewx.METRIC
        packet['dateTime'] = data['dateTime']

        # include the link status - 0 indicates ok, 1 indicates no link
        data['link_wind'] = 0 if data['windspeed_state'] == STATE_OK else 1
        data['link_rain'] = 0 if data['rain_state'] == STATE_OK else 1
        data['link_uv'] = 0 if data['uv_state'] == STATE_OK else 1
        data['link_1'] = 0 if data['t_1_state'] == STATE_OK else 1
        data['link_2'] = 0 if data['t_2_state'] == STATE_OK else 1
        data['link_3'] = 0 if data['t_3_state'] == STATE_OK else 1
        data['link_4'] = 0 if data['t_4_state'] == STATE_OK else 1
        data['link_5'] = 0 if data['t_5_state'] == STATE_OK else 1

        # map extensible sensors to database fields
        for label in sensor_map:
            if sensor_map[label] in data:
                packet[label] = data[sensor_map[label]]
            elif status is not None and sensor_map[label] in status:
                packet[label] = int(status[sensor_map[label]])

        # handle unit converstions
        packet['windSpeed'] = data.get('windspeed')
        if packet['windSpeed'] is not None:
            packet['windSpeed'] *= 1.60934 # speed is mph; weewx wants km/h
        packet['windDir'] = data.get('winddir')
        if packet['windDir'] is not None:
            packet['windDir'] *= 22.5 # weewx wants degrees

        packet['windGust'] = data.get('windgust')
        if packet['windGust'] is not None:
            packet['windGust'] *= 1.60934 # speed is mph; weewx wants km/h

        packet['rainTotal'] = data['rain']
        if packet['rainTotal'] is not None:
            packet['rainTotal'] *= 0.0705555556 # weewx wants cm (1/36 inch)
        packet['rain'] = weewx.wxformulas.calculate_rain(
            packet['rainTotal'], last_rain)

        # some stations report uv
        packet['UV'] = data['uv']

        # station calculates windchill
        packet['windchill'] = data['windchill']

        # station reports baromter (SLP)
        packet['barometer'] = data['slp']

        # forecast and storm fields use the station's algorithms
        packet['forecast'] = data['forecast']
        packet['storm'] = data['storm']

        return packet


STATE_OK = 'ok'
STATE_INVALID = 'invalid'
STATE_NO_LINK = 'no_link'

def _fmt(buf):
    if buf:
        return ' '.join(["%02x" % x for x in buf])
    return ''

def bcd2int(bcd):
    return int(((bcd & 0xf0) >> 4) * 10) + int(bcd & 0x0f)

def rev_bcd2int(bcd):
    return int((bcd & 0xf0) >> 4) + int((bcd & 0x0f) * 10)

def int2bcd(num):
    return int(num / 10) * 0x10 + (num % 10)

def rev_int2bcd(num):
    return (num % 10) * 0x10 + int(num / 10)

def decode(buf):
    data = dict()
    for i in range(6):  # console plus 5 remote channels
        data.update(decode_th(buf, i))
    data.update(decode_uv(buf))
    data.update(decode_pressure(buf))
    data.update(decode_forecast(buf))
    data.update(decode_windchill(buf))
    data.update(decode_wind(buf))
    data.update(decode_rain(buf))
    return data

def decode_th(buf, i):
    if i == 0:
        tlabel = 't_in'
        hlabel = 'h_in'
    else:
        tlabel = 't_%d' % i
        hlabel = 'h_%d' % i
    tstate = '%s_state' % tlabel
    hstate = '%s_state' % hlabel
    offset = i * 3

    if DEBUG_DECODE:
        log.debug("TH%d  BUF[%02d]=%02x BUF[%02d]=%02x BUF[%02d]=%02x" %
               (i, 0 + offset, buf[0 + offset], 1 + offset, buf[1 + offset],
                2 + offset, buf[2 + offset]))
    data = dict()
    data[tlabel], data[tstate] = decode_temp(buf[0 + offset], buf[1 + offset],
                                             i != 0)
    data[hlabel], data[hstate] = decode_humid(buf[2 + offset])
    if DEBUG_DECODE:
        log.debug("TH%d  %s %s %s %s" % (i, data[tlabel], data[tstate],
                                      data[hlabel], data[hstate]))
    return data

def decode_temp(byte1, byte2, remote):
    """decode temperature.  result is degree C."""
    if bcd2int(byte1 & 0x0f) > 9:
        if byte1 & 0x0f == 0x0a:
            return None, STATE_NO_LINK
        else:
            return None, STATE_INVALID
    if byte2 & 0x40 != 0x40 and remote:
        return None, STATE_INVALID
    value = bcd2int(byte1) / 10.0 + bcd2int(byte2 & 0x0f) * 10.0
    if byte2 & 0x20 == 0x20:
        value += 0.05
    if byte2 & 0x80 != 0x80:
        value *= -1
    return value, STATE_OK

def decode_humid(byte):
    """decode humidity.  result is percentage."""
    if bcd2int(byte & 0x0f) > 9:
        if byte & 0x0f == 0x0a:
            return None, STATE_NO_LINK
        else:
            return None, STATE_INVALID
    return bcd2int(byte), STATE_OK

# NB: te923tool does not include the 4-bit shift
def decode_uv(buf):
    """decode data from uv sensor"""
    data = dict()
    if DEBUG_DECODE:
        log.debug("UVX  BUF[18]=%02x BUF[19]=%02x" % (buf[18], buf[19]))
    if ((buf[18] == 0xaa and buf[19] == 0x0a) or
        (buf[18] == 0xff and buf[19] == 0xff)):
        data['uv_state'] = STATE_NO_LINK
        data['uv'] = None
    elif bcd2int(buf[18]) > 99 or bcd2int(buf[19]) > 99:
        data['uv_state'] = STATE_INVALID
        data['uv'] = None
    else:
        data['uv_state'] = STATE_OK
        data['uv'] = bcd2int(buf[18] & 0x0f) / 10.0 \
            + bcd2int((buf[18] & 0xf0) >> 4) \
            + bcd2int(buf[19] & 0x0f) * 10.0
    if DEBUG_DECODE:
        log.debug("UVX  %s %s" % (data['uv'], data['uv_state']))
    return data

def decode_pressure(buf):
    """decode pressure data"""
    data = dict()
    if DEBUG_DECODE:
        log.debug("PRS  BUF[20]=%02x BUF[21]=%02x" % (buf[20], buf[21]))
    if buf[21] & 0xf0 == 0xf0:
        data['slp_state'] = STATE_INVALID
        data['slp'] = None
    else:
        data['slp_state'] = STATE_OK
        data['slp'] = int(buf[21] * 0x100 + buf[20]) * 0.0625
    if DEBUG_DECODE:
        log.debug("PRS  %s %s" % (data['slp'], data['slp_state']))
    return data

# NB: te923tool divides speed/gust by 2.23694 (1 meter/sec = 2.23694 mile/hour)
# NB: wview does not divide speed/gust
# NB: wview multiplies winddir by 22.5, te923tool does not
def decode_wind(buf):
    """decode wind speed, gust, and direction"""
    data = dict()
    if DEBUG_DECODE:
        log.debug("WGS  BUF[25]=%02x BUF[26]=%02x" % (buf[25], buf[26]))
    data['windgust'], data['windgust_state'] = decode_ws(buf[25], buf[26])
    if DEBUG_DECODE:
        log.debug("WGS  %s %s" % (data['windgust'], data['windgust_state']))

    if DEBUG_DECODE:
        log.debug("WSP  BUF[27]=%02x BUF[28]=%02x" % (buf[27], buf[28]))
    data['windspeed'], data['windspeed_state'] = decode_ws(buf[27], buf[28])
    if DEBUG_DECODE:
        log.debug("WSP  %s %s" % (data['windspeed'], data['windspeed_state']))

    if DEBUG_DECODE:
        log.debug("WDR  BUF[29]=%02x" % buf[29])
    data['winddir_state'] = data['windspeed_state']
    data['winddir'] = int(buf[29] & 0x0f)
    if DEBUG_DECODE:
        log.debug("WDR  %s %s" % (data['winddir'], data['winddir_state']))
    
    return data

def decode_ws(byte1, byte2):
    """decode wind speed, result is mph"""
    if bcd2int(byte1 & 0xf0) > 90 or bcd2int(byte1 & 0x0f) > 9:
        if ((byte1 == 0xee and byte2 == 0x8e) or
            (byte1 == 0xff and byte2 == 0xff)):
            return None, STATE_NO_LINK
        else:
            return None, STATE_INVALID
    offset = 100 if byte2 & 0x10 == 0x10 else 0
    value = bcd2int(byte1) / 10.0 + bcd2int(byte2 & 0x0f) * 10.0 + offset
    return value, STATE_OK

# the rain counter is in the station, not the rain bucket.  so if the link
# between rain bucket and station is lost, the station will miss rainfall and
# there is no way to know about it.
# FIXME: figure out how to detect link status between station and rain bucket
# NB: wview treats the raw rain count as millimeters
def decode_rain(buf):
    """rain counter is number of bucket tips, each tip is about 0.03 inches"""
    data = dict()
    if DEBUG_DECODE:
        log.debug("RAIN BUF[30]=%02x BUF[31]=%02x" % (buf[30], buf[31]))
    data['rain_state'] = STATE_OK
    data['rain'] = int(buf[31] * 0x100 + buf[30])
    if DEBUG_DECODE:
        log.debug("RAIN %s %s" % (data['rain'], data['rain_state']))
    return data

def decode_windchill(buf):
    data = dict()
    if DEBUG_DECODE:
        log.debug("WCL  BUF[23]=%02x BUF[24]=%02x" % (buf[23], buf[24]))
    if bcd2int(buf[23] & 0xf0) > 90 or bcd2int(buf[23] & 0x0f) > 9:
        if ((buf[23] == 0xee and buf[24] == 0x8e) or
            (buf[23] == 0xff and buf[24] == 0xff)):
            data['windchill_state'] = STATE_NO_LINK
        else:
            data['windchill_state'] = STATE_INVALID
        data['windchill'] = None
    elif buf[24] & 0x40 != 0x40:
        data['windchill_state'] = STATE_INVALID
        data['windchill'] = None
    else:
        data['windchill_state'] = STATE_OK
        data['windchill'] = bcd2int(buf[23]) / 10.0 \
            + bcd2int(buf[24] & 0x0f) * 10.0
        if buf[24] & 0x20 == 0x20:
            data['windchill'] += 0.05
        if buf[24] & 0x80 != 0x80:
            data['windchill'] *= -1
    if DEBUG_DECODE:
        log.debug("WCL  %s %s" % (data['windchill'], data['windchill_state']))
    return data

def decode_forecast(buf):
    data = dict()
    if DEBUG_DECODE:
        log.debug("STT  BUF[22]=%02x" % buf[22])
    if buf[22] & 0x0f == 0x0f:
        data['storm'] = None
        data['forecast'] = None
    else:
        data['storm'] = 1 if buf[22] & 0x08 == 0x08 else 0
        data['forecast'] = int(buf[22] & 0x07)
    if DEBUG_DECODE:
        log.debug("STT  %s %s" % (data['storm'], data['forecast']))
    return data


class BadRead(weewx.WeeWxIOError):
    """Bogus data length, CRC, header block, or other read failure"""

class BadWrite(weewx.WeeWxIOError):
    """Bogus data length, header block, or other write failure"""

class BadHeader(weewx.WeeWxIOError):
    """Bad header byte"""

class TE923Station(object):
    ENDPOINT_IN = 0x81
    READ_LENGTH = 0x8
    TIMEOUT = 1200
    START_ADDRESS = 0x101
    RECORD_SIZE = 0x26

    idx_to_interval_sec = {
        1: 300, 2: 600, 3: 1200, 4: 1800, 5: 3600, 6: 5400, 7: 7200,
        8: 10800, 9: 14400, 10: 21600, 11: 86400}

    def __init__(self, vendor_id=0x1130, product_id=0x6801,
                 max_tries=10, retry_wait=5, read_timeout=5):
        self.vendor_id = vendor_id
        self.product_id = product_id
        self.devh = None
        self.max_tries = max_tries
        self.retry_wait = retry_wait
        self.read_timeout = read_timeout

        self._num_rec = None
        self._num_blk = None

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

    def __exit__(self, type_, value, traceback):  # @UnusedVariable
        self.close()

    def open(self, interface=0):
        dev = self._find_dev(self.vendor_id, self.product_id)
        if not dev:
            log.critical("Cannot find USB device with VendorID=0x%04x ProductID=0x%04x" % (self.vendor_id, self.product_id))
            raise weewx.WeeWxIOError('Unable to find station on USB')

        self.devh = dev.open()
        if not self.devh:
            raise weewx.WeeWxIOError('Open USB device failed')

        # be sure kernel does not claim the interface
        try:
            self.devh.detachKernelDriver(interface)
        except (AttributeError, usb.USBError):
            pass

        # attempt to claim the interface
        try:
            self.devh.claimInterface(interface)
            self.devh.setAltInterface(interface)
        except usb.USBError as e:
            self.close()
            log.critical("Unable to claim USB interface %s: %s" % (interface, e))
            raise weewx.WeeWxIOError(e)

# doing a reset seems to cause problems more often than it eliminates them
#        self.devh.reset()

        # figure out which type of memory this station has
        self.read_memory_size()

    def close(self):
        try:
            self.devh.releaseInterface()
        except (ValueError, usb.USBError) as e:
            log.error("release interface failed: %s" % e)
        self.devh = None

    @staticmethod
    def _find_dev(vendor_id, product_id):
        """Find the vendor and product ID on the USB."""
        for bus in usb.busses():
            for dev in bus.devices:
                if dev.idVendor == vendor_id and dev.idProduct == product_id:
                    log.info('Found device on USB bus=%s device=%s' %
                           (bus.dirname, dev.filename))
                    return dev
        return None

    def _raw_read(self, addr):
        reqbuf = [0x05, 0xAF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
        reqbuf[4] = addr // 0x10000
        reqbuf[3] = (addr - (reqbuf[4] * 0x10000)) // 0x100
        reqbuf[2] = addr - (reqbuf[4] * 0x10000) - (reqbuf[3] * 0x100)
        reqbuf[5] = (reqbuf[1] ^ reqbuf[2] ^ reqbuf[3] ^ reqbuf[4])
        ret = self.devh.controlMsg(requestType=0x21,
                                   request=usb.REQ_SET_CONFIGURATION,
                                   value=0x0200,
                                   index=0x0000,
                                   buffer=reqbuf,
                                   timeout=self.TIMEOUT)
        if ret != 8:
            raise BadRead('Unexpected response to data request: %s != 8' % ret)

# sleeping does not seem to have any effect on the reads
#        time.sleep(0.1)  # te923tool is 0.3
        start_ts = time.time()
        rbuf = []
        while time.time() - start_ts < self.read_timeout:
            try:
                buf = self.devh.interruptRead(
                    self.ENDPOINT_IN, self.READ_LENGTH, self.TIMEOUT)
                if buf:
                    nbytes = buf[0]
                    if nbytes > 7 or nbytes > len(buf) - 1:
                        raise BadRead("Bogus length during read: %d" % nbytes)
                    rbuf.extend(buf[1:1 + nbytes])
                if len(rbuf) >= 34:
                    break
            except usb.USBError as e:
                errmsg = repr(e)
                if not ('No data available' in errmsg or 'No error' in errmsg):
                    raise
# sleeping seems to have no effect on the reads
#            time.sleep(0.009) # te923tool is 0.15
        else:
            log.debug("timeout while reading: ignoring bytes: %s" % _fmt(rbuf))
            raise BadRead("Timeout after %d bytes" % len(rbuf))

        # Send acknowledgement whether or not it was a good read
        reqbuf = [0x24, 0xAF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
        reqbuf[4] = addr // 0x10000
        reqbuf[3] = (addr - (reqbuf[4] * 0x10000)) // 0x100
        reqbuf[2] = addr - (reqbuf[4] * 0x10000) - (reqbuf[3] * 0x100)
        reqbuf[5] = (reqbuf[1] ^ reqbuf[2] ^ reqbuf[3] ^ reqbuf[4])
        ret = self.devh.controlMsg(requestType=0x21,
                                   request=usb.REQ_SET_CONFIGURATION,
                                   value=0x0200,
                                   index=0x0000,
                                   buffer=reqbuf,
                                   timeout=self.TIMEOUT)

        # now check what we got
        if len(rbuf) < 34:
            raise BadRead("Not enough bytes: %d < 34" % len(rbuf))
        # there must be a header byte...
        if rbuf[0] != 0x5a:
            raise BadHeader("Bad header byte: %02x != %02x" % (rbuf[0], 0x5a))
        # ...and the last byte must be a valid crc
        crc = 0x00
        for x in rbuf[:33]:
            crc = crc ^ x
        if crc != rbuf[33]:
            raise BadRead("Bad crc: %02x != %02x" % (crc, rbuf[33]))

        # early versions of this driver used to get long reads, but these
        # might not happen any more. log it then try to use the data anyway.
        if len(rbuf) != 34:
            log.info("read: wrong number of bytes: %d != 34" % len(rbuf))

        return rbuf

    def _raw_write(self, addr, buf):
        wbuf = [0] * 38
        wbuf[0] = 0xAE
        wbuf[3] = addr // 0x10000
        wbuf[2] = (addr - (wbuf[3] * 0x10000)) // 0x100
        wbuf[1] = addr - (wbuf[3] * 0x10000) - (wbuf[2] * 0x100)
        crc = wbuf[0] ^ wbuf[1] ^ wbuf[2] ^ wbuf[3]
        for i in range(32):
            wbuf[i + 4] = buf[i]
            crc = crc ^ buf[i]
        wbuf[36] = crc
        for i in range(6):
            if i == 5:
                reqbuf = [0x2,
                          wbuf[i * 7], wbuf[1 + i * 7],
                          0x00, 0x00, 0x00, 0x00, 0x00]
            else:
                reqbuf = [0x7,
                          wbuf[i * 7], wbuf[1 + i * 7], wbuf[2 + i * 7],
                          wbuf[3 + i * 7], wbuf[4 + i * 7], wbuf[5 + i * 7],
                          wbuf[6 + i * 7]]
            if DEBUG_WRITE:
                log.debug("write: %s" % _fmt(reqbuf))
            ret = self.devh.controlMsg(requestType=0x21,
                                       request=usb.REQ_SET_CONFIGURATION,
                                       value=0x0200,
                                       index=0x0000,
                                       buffer=reqbuf,
                                       timeout=self.TIMEOUT)
            if ret != 8:
                raise BadWrite('Unexpected response: %s != 8' % ret)
            
        # Wait for acknowledgement
        time.sleep(0.1)
        start_ts = time.time()
        rbuf = []
        while time.time() - start_ts < 5:
            try:
                tmpbuf = self.devh.interruptRead(
                    self.ENDPOINT_IN, self.READ_LENGTH, self.TIMEOUT)
                if tmpbuf:
                    nbytes = tmpbuf[0]
                    if nbytes > 7 or nbytes > len(tmpbuf) - 1:
                        raise BadRead("Bogus length during read: %d" % nbytes)
                    rbuf.extend(tmpbuf[1:1 + nbytes])
                if len(rbuf) >= 1:
                    break
            except usb.USBError as e:
                errmsg = repr(e)
                if not ('No data available' in errmsg or 'No error' in errmsg):
                    raise
            time.sleep(0.009)
        else:
            raise BadWrite("Timeout after %d bytes" % len(rbuf))

        if len(rbuf) != 1:
            log.info("write: ack got wrong number of bytes: %d != 1" % len(rbuf))
        if len(rbuf) == 0:
            raise BadWrite("Bad ack: zero length response")
        elif rbuf[0] != 0x5a:
            raise BadHeader("Bad header byte: %02x != %02x" % (rbuf[0], 0x5a))

    def _read(self, addr):
        """raw_read returns the entire 34-byte chunk, i.e., one header byte,
        32 data bytes, one checksum byte.  this function simply returns it."""
        # FIXME: strip the header and checksum so that we return only the
        #        32 bytes of data.  this will require shifting every index
        #        pretty much everywhere else in this code.
        if DEBUG_READ:
            log.debug("read: address 0x%06x" % addr)
        for cnt in range(self.max_tries):
            try:
                buf = self._raw_read(addr)
                if DEBUG_READ:
                    log.debug("read: %s" % _fmt(buf))
                return buf
            except (BadRead, BadHeader, usb.USBError) as e:
                log.error("Failed attempt %d of %d to read data: %s" %
                       (cnt + 1, self.max_tries, e))
                log.debug("Waiting %d seconds before retry" % self.retry_wait)
                time.sleep(self.retry_wait)
        else:
            raise weewx.RetriesExceeded("Read failed after %d tries" %
                                        self.max_tries)

    def _write(self, addr, buf):
        if DEBUG_WRITE:
            log.debug("write: address 0x%06x: %s" % (addr, _fmt(buf)))
        for cnt in range(self.max_tries):
            try:
                self._raw_write(addr, buf)
                return
            except (BadWrite, BadHeader, usb.USBError) as e:
                log.error("Failed attempt %d of %d to write data: %s" %
                       (cnt + 1, self.max_tries, e))
                log.debug("Waiting %d seconds before retry" % self.retry_wait)
                time.sleep(self.retry_wait)
        else:
            raise weewx.RetriesExceeded("Write failed after %d tries" %
                                        self.max_tries)

    def read_memory_size(self):
        buf = self._read(0xfc)
        if DEBUG_DECODE:
            log.debug("MEM  BUF[1]=%s" % buf[1])
        if buf[1] == 0:
            self._num_rec = 208
            self._num_blk = 256
            log.debug("detected small memory size")
        elif buf[1] == 2:
            self._num_rec = 3442
            self._num_blk = 4096
            log.debug("detected large memory size")
        else:
            msg = "Unrecognised memory size '%s'" % buf[1]
            log.error(msg)
            raise weewx.WeeWxIOError(msg)

    def get_memory_size(self):
        return self._num_rec

    def gen_blocks(self, count=None):
        """generator that returns consecutive blocks of station memory"""
        if not count:
            count = self._num_blk
        for x in range(0, count * 32, 32):
            buf = self._read(x)
            yield x, buf

    def dump_memory(self):
        for i in range(8):
            buf = self._read(i * 32)
            for j in range(4):
                log.info("%02x : %02x %02x %02x %02x %02x %02x %02x %02x" %
                       (i * 32 + j * 8, buf[1 + j * 8], buf[2 + j * 8],
                        buf[3 + j * 8], buf[4 + j * 8], buf[5 + j * 8],
                        buf[6 + j * 8], buf[7 + j * 8], buf[8 + j * 8]))

    def get_config(self):
        data = dict()
        data.update(self.get_versions())
        data.update(self.get_status())
        data['latitude'], data['longitude'] = self.get_location()
        data['altitude'] = self.get_altitude()
        return data

    def get_versions(self):
        data = dict()
        buf = self._read(0x98)
        if DEBUG_DECODE:
            log.debug("VER  BUF[1]=%s BUF[2]=%s BUF[3]=%s BUF[4]=%s BUF[5]=%s" %
                   (buf[1], buf[2], buf[3], buf[4], buf[5]))
        data['version_bar']  = buf[1]
        data['version_uv']   = buf[2]
        data['version_rcc']  = buf[3]
        data['version_wind'] = buf[4]
        data['version_sys']  = buf[5]
        if DEBUG_DECODE:
            log.debug("VER  bar=%s uv=%s rcc=%s wind=%s sys=%s" %
                   (data['version_bar'], data['version_uv'],
                    data['version_rcc'], data['version_wind'],
                    data['version_sys']))
        return data

    def get_status(self):
        # map the battery status flags.  0 indicates ok, 1 indicates failure.
        # FIXME: i get 1 for uv even when no uv link
        # FIXME: i get 0 for th3, th4, th5 even when no link
        status = dict()
        buf = self._read(0x4c)
        if DEBUG_DECODE:
            log.debug("BAT  BUF[1]=%02x" % buf[1])
        status['bat_rain'] = 0 if buf[1] & 0x80 == 0x80 else 1
        status['bat_wind'] = 0 if buf[1] & 0x40 == 0x40 else 1
        status['bat_uv']   = 0 if buf[1] & 0x20 == 0x20 else 1
        status['bat_5']    = 0 if buf[1] & 0x10 == 0x10 else 1
        status['bat_4']    = 0 if buf[1] & 0x08 == 0x08 else 1
        status['bat_3']    = 0 if buf[1] & 0x04 == 0x04 else 1
        status['bat_2']    = 0 if buf[1] & 0x02 == 0x02 else 1
        status['bat_1']    = 0 if buf[1] & 0x01 == 0x01 else 1
        if DEBUG_DECODE:
            log.debug("BAT  rain=%s wind=%s uv=%s th5=%s th4=%s th3=%s th2=%s th1=%s" %
                   (status['bat_rain'], status['bat_wind'], status['bat_uv'],
                    status['bat_5'], status['bat_4'], status['bat_3'],
                    status['bat_2'], status['bat_1']))
        return status

    # FIXME: is this any different than get_alt?
    def get_altitude(self):
        buf = self._read(0x5a)
        if DEBUG_DECODE:
            log.debug("ALT  BUF[1]=%02x BUF[2]=%02x BUF[3]=%02x" %
                   (buf[1], buf[2], buf[3]))
        altitude = buf[2] * 0x100 + buf[1]
        if buf[3] & 0x8 == 0x8:
            altitude *= -1
        if DEBUG_DECODE:
            log.debug("ALT  %s" % altitude)
        return altitude

    # FIXME: is this any different than get_loc?
    def get_location(self):
        buf = self._read(0x06)
        if DEBUG_DECODE:
            log.debug("LOC  BUF[1]=%02x BUF[2]=%02x BUF[3]=%02x BUF[4]=%02x BUF[5]=%02x BUF[6]=%02x" % (buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]))
        latitude = float(rev_bcd2int(buf[1])) + (float(rev_bcd2int(buf[2])) / 60)
        if buf[5] & 0x80 == 0x80:
            latitude *= -1
        longitude = float((buf[6] & 0xf0) // 0x10 * 100) + float(rev_bcd2int(buf[3])) + (float(rev_bcd2int(buf[4])) / 60)
        if buf[5] & 0x40 == 0x00:
            longitude *= -1
        if DEBUG_DECODE:
            log.debug("LOC  %s %s" % (latitude, longitude))
        return latitude, longitude

    def get_readings(self):
        """get sensor readings from the station, return as dictionary"""
        buf = self._read(0x020001)
        data = decode(buf[1:])
        data['dateTime'] = int(time.time() + 0.5)
        return data

    def _get_next_index(self):
        """get the index of the next history record"""
        buf = self._read(0xfb)
        if DEBUG_DECODE:
            log.debug("HIS  BUF[3]=%02x BUF[5]=%02x" % (buf[3], buf[5]))
        record_index = buf[3] * 0x100 + buf[5]
        log.debug("record_index=%s" % record_index)
        if record_index > self._num_rec:
            msg = "record index of %d exceeds memory size of %d records" % (
                record_index, self._num_rec)
            log.error(msg)
            raise weewx.WeeWxIOError(msg)
        return record_index

    def _get_starting_addr(self, requested):
        """calculate the oldest and latest addresses"""
        count = requested
        if count is None:
            count = self._num_rec
        elif count > self._num_rec:
            count = self._num_rec
            log.info("too many records requested (%d), using %d instead" %
                   (requested, count))
        idx = self._get_next_index()
        if idx < 1:
            idx += self._num_rec
        latest_addr = self.START_ADDRESS + (idx - 1) * self.RECORD_SIZE
        oldest_addr = latest_addr - (count - 1) * self.RECORD_SIZE
        log.debug("count=%s oldest_addr=0x%06x latest_addr=0x%06x" %
                  (count, oldest_addr, latest_addr))
        return oldest_addr, count

    def gen_records(self, since_ts=0, requested=None):
        """return requested records from station from oldest to newest.  If
        since_ts is specified, then all records since that time.  If requested
        is specified, then at most that many most recent records.  If both
        are specified then at most requested records newer than the timestamp.

        Each historical record is 38 bytes (0x26) long.  Records start at
        memory address 0x101 (257).  The index of the record after the latest
        is at address 0xfc:0xff (253:255), indicating the offset from the
        starting address.

        On small memory stations, the last 32 bytes of memory are never used.
        On large memory stations, the last 20 bytes of memory are never used.
        """

        log.debug("gen_records: since_ts=%s requested=%s" % (since_ts, requested))
        # we need the current year and month since station does not track year
        start_ts = time.time()
        tt = time.localtime(start_ts)
        # get the archive interval for use in calculations later
        arcint = self.get_interval_seconds()
        # if nothing specified, get everything since time began
        if since_ts is None:
            since_ts = 0
        # if no count specified, use interval to estimate number of records
        if requested is None:
            requested = int((start_ts - since_ts) / arcint)
            requested += 1 # safety margin
        # get the starting address for what we want to read, plus actual count
        oldest_addr, count = self._get_starting_addr(requested)
        # inner loop reads records, outer loop catches any added while reading
        more_records = True
        while more_records:
            n = 0
            while n < count:
                addr = oldest_addr + n * self.RECORD_SIZE
                if addr < self.START_ADDRESS:
                    addr += self._num_rec * self.RECORD_SIZE
                record = self.get_record(addr, tt.tm_year, tt.tm_mon)
                n += 1
                msg = "record %d of %d addr=0x%06x" % (n, count, addr)
                if record and record['dateTime'] > since_ts:
                    msg += " %s" % timestamp_to_string(record['dateTime'])
                    log.debug("gen_records: yield %s" % msg)
                    yield record
                else:
                    if record:
                        msg += " since_ts=%d %s" % (
                            since_ts, timestamp_to_string(record['dateTime']))
                    log.debug("gen_records: skip %s" % msg)
                # insert a sleep to simulate slow reads
#                time.sleep(5)

            # see if reading has taken so much time that more records have
            # arrived. read whatever records have come in since the read began.
            now = time.time()
            if now - start_ts > arcint:
                newreq = int((now - start_ts) / arcint)
                newreq += 1 # safety margin
                log.debug("gen_records: reading %d more records" % newreq)
                oldest_addr, count = self._get_starting_addr(newreq)
                start_ts = now
            else:
                more_records = False

    def get_record(self, addr, now_year, now_month):
        """Return a single record from station."""

        log.debug("get_record at address 0x%06x (year=%s month=%s)" %
               (addr, now_year, now_month))
        buf = self._read(addr)
        if DEBUG_DECODE:
            log.debug("REC  %02x %02x %02x %02x" %
                   (buf[1], buf[2], buf[3], buf[4]))
        if buf[1] == 0xff:
            log.debug("get_record: no data at address 0x%06x" % addr)
            return None
        
        year = now_year
        month = buf[1] & 0x0f
        if month > now_month:
            year -= 1
        day = bcd2int(buf[2])
        hour = bcd2int(buf[3])
        minute = bcd2int(buf[4])
        ts = time.mktime((year, month, day, hour, minute, 0, 0, 0, -1))
        if DEBUG_DECODE:
            log.debug("REC  %d/%02d/%02d %02d:%02d = %d" %
                   (year, month, day, hour, minute, ts))

        tmpbuf = buf[5:16]
        buf = self._read(addr + 0x10) 
        tmpbuf.extend(buf[1:22])
        
        data = decode(tmpbuf)
        data['dateTime'] = int(ts)
        log.debug("get_record: found record %s" % data)
        return data

    def _read_minmax(self):
        buf = self._read(0x24)
        tmpbuf = self._read(0x40)
        buf[28:37] = tmpbuf[1:10]
        tmpbuf = self._read(0xaa)
        buf[37:47] = tmpbuf[1:11]
        tmpbuf = self._read(0x60)
        buf[47:74] = tmpbuf[1:28]
        tmpbuf = self._read(0x7c)
        buf[74:101] = tmpbuf[1:28]
        return buf

    def get_minmax(self):
        buf = self._read_minmax()
        data = dict()
        data['t_in_min'], _ = decode_temp(buf[1], buf[2], 0)
        data['t_in_max'], _ = decode_temp(buf[3], buf[4], 0)
        data['h_in_min'], _ = decode_humid(buf[5])
        data['h_in_max'], _ = decode_humid(buf[6])
        for i in range(5):
            label = 't_%d_%%s' % (i + 1)
            data[label % 'min'], _ = decode_temp(buf[7+i*6], buf[8 +i*6], 1)
            data[label % 'max'], _ = decode_temp(buf[9+i*6], buf[10+i*6], 1)
            label = 'h_%d_%%s' % (i + 1)
            data[label % 'min'], _ = decode_humid(buf[11+i*6])
            data[label % 'max'], _ = decode_humid(buf[12+i*6])
        data['windspeed_max'], _ = decode_ws(buf[37], buf[38])
        data['windgust_max'], _ = decode_ws(buf[39], buf[40])
        # not sure if this is the correct units here...
        data['rain_yesterday'] = (buf[42] * 0x100 + buf[41]) * 0.705555556
        data['rain_week'] = (buf[44] * 0x100 + buf[43]) * 0.705555556
        data['rain_month'] = (buf[46] * 0x100 + buf[45]) * 0.705555556
        tt = time.localtime()
        offset = 1 if tt[3] < 12 else 0
        month = bcd2int(buf[47] & 0xf)
        day = bcd2int(buf[48])
        hour = bcd2int(buf[49])
        minute = bcd2int(buf[50])
        year = tt.tm_year
        if month > tt.tm_mon:
            year -= 1
        ts = time.mktime((year, month, day - offset, hour, minute, 0, 0, 0, 0))
        data['barometer_ts'] = ts
        for i in range(25):
            data['barometer_%d' % i] = (buf[52+i*2]*0x100 + buf[51+i*2])*0.0625
        return data

    def _read_date(self):
        buf = self._read(0x0)
        return buf[1:33]

    def _write_date(self, buf):
        self._write(0x0, buf)

    def get_date(self):
        tt = time.localtime()
        offset = 1 if tt[3] < 12 else 0
        buf = self._read_date()
        day = rev_bcd2int(buf[2])
        month = (buf[5] & 0xF0) // 0x10
        year = rev_bcd2int(buf[4]) + 2000
        ts = time.mktime((year, month, day + offset, 0, 0, 0, 0, 0, 0))
        return ts

    def set_date(self, ts):
        tt = time.localtime(ts)
        buf = self._read_date()
        buf[2] = rev_int2bcd(tt[2])
        buf[4] = rev_int2bcd(tt[0] - 2000)
        buf[5] = tt[1] * 0x10 + (tt[6] + 1) * 2 + (buf[5] & 1)
        buf[15] = self._checksum(buf[0:15])
        self._write_date(buf)

    def _read_loc(self, loc_type):
        addr = 0x0 if loc_type == 0 else 0x16
        buf = self._read(addr)
        return buf[1:33]
    
    def _write_loc(self, loc_type, buf):
        addr = 0x0 if loc_type == 0 else 0x16
        self._write(addr, buf)

    def get_loc(self, loc_type):
        buf = self._read_loc(loc_type)
        offset = 6 if loc_type == 0 else 0
        data = dict()
        data['city_time'] = (buf[6 + offset] & 0xF0) + (buf[7 + offset] & 0xF)
        data['lat_deg'] = rev_bcd2int(buf[0 + offset])
        data['lat_min'] = rev_bcd2int(buf[1 + offset])
        data['lat_dir'] = "S" if buf[4 + offset] & 0x80 == 0x80 else "N"
        data['long_deg'] = (buf[5 + offset] & 0xF0) // 0x10 * 100 + rev_bcd2int(buf[2 + offset])
        data['long_min'] = rev_bcd2int(buf[3 + offset])
        data['long_dir'] = "E" if buf[4 + offset] & 0x40 == 0x40 else "W"
        data['tz_hr'] = (buf[7 + offset] & 0xF0) // 0x10
        if buf[4 + offset] & 0x8 == 0x8:
            data['tz_hr'] *= -1
        data['tz_min'] = 30 if buf[4 + offset] & 0x3 == 0x3 else 0
        if buf[4 + offset] & 0x10 == 0x10:
            data['dst_always_on'] = True
        else:
            data['dst_always_on'] = False
            data['dst'] = buf[5 + offset] & 0xf
        return data

    def set_loc(self, loc_type, city_index, dst_on, dst_index, tz_hr, tz_min,
                lat_deg, lat_min, lat_dir, long_deg, long_min, long_dir):
        buf = self._read_loc(loc_type)
        offset = 6 if loc_type == 0 else 0
        buf[0 + offset] = rev_int2bcd(lat_deg)
        buf[1 + offset] = rev_int2bcd(lat_min)
        buf[2 + offset] = rev_int2bcd(long_deg % 100)
        buf[3 + offset] = rev_int2bcd(long_min)
        buf[4 + offset] = (lat_dir == "S") * 0x80 + (long_dir == "E") * 0x40 + (tz_hr < 0) + dst_on * 0x10 * 0x8 + (tz_min == 30) * 3
        buf[5 + offset] = (long_deg > 99) * 0x10 + dst_index
        buf[6 + offset] = (buf[28] & 0x0F) + int(city_index / 0x10) * 0x10
        buf[7 + offset] = city_index % 0x10 + abs(tz_hr) * 0x10
        if loc_type == 0:
            buf[15] = self._checksum(buf[0:15])
        else:
            buf[8] = self._checksum(buf[0:8])
        self._write_loc(loc_type, buf)

    def _read_alt(self):
        buf = self._read(0x5a)
        return buf[1:33]

    def _write_alt(self, buf):
        self._write(0x5a, buf)

    def get_alt(self):
        buf = self._read_alt()
        altitude = buf[1] * 0x100 + buf[0]
        if buf[3] & 0x8 == 0x8:
            altitude *= -1
        return altitude

    def set_alt(self, altitude):
        buf = self._read_alt()
        buf[0] = abs(altitude) & 0xff
        buf[1] = abs(altitude) // 0x100
        buf[2] = buf[2] & 0x7 + (altitude < 0) * 0x8
        buf[3] = self._checksum(buf[0:3])
        self._write_alt(buf)

    def _read_alarms(self):
        buf = self._read(0x10)
        tmpbuf = self._read(0x1F)
        buf[33:65] = tmpbuf[1:33]
        tmpbuf = self._read(0xA0)
        buf[65:97] = tmpbuf[1:33]
        return buf[1:97]

    def _write_alarms(self, buf):
        self._write(0x10, buf[0:32])
        self._write(0x1F, buf[32:64])
        self._write(0xA0, buf[64:96])
        
    def get_alarms(self):
        buf = self._read_alarms()
        data = dict()
        data['weekday_active'] = buf[0] & 0x4 == 0x4
        data['single_active'] = buf[0] & 0x8 == 0x8
        data['prealarm_active'] = buf[2] & 0x8 == 0x8
        data['weekday_hour'] = rev_bcd2int(buf[0] & 0xF1)
        data['weekday_min'] = rev_bcd2int(buf[1])
        data['single_hour'] = rev_bcd2int(buf[2] & 0xF1)
        data['single_min'] = rev_bcd2int(buf[3])
        data['prealarm_period'] = (buf[4] & 0xF0) // 0x10
        data['snooze'] = buf[4] & 0xF
        data['max_temp'], _ = decode_temp(buf[32], buf[33], 0)
        data['min_temp'], _ = decode_temp(buf[34], buf[35], 0)
        data['rain_active'] = buf[64] & 0x4 == 0x4
        data['windspeed_active'] = buf[64] & 0x2 == 0x2
        data['windgust_active'] = buf[64] & 0x1 == 0x1
        data['rain'] = bcd2int(buf[66]) * 100 + bcd2int(buf[65])
        data['windspeed'], _ = decode_ws(buf[68], buf[69])
        data['windgust'], _ = decode_ws(buf[71], buf[72])
        return data

    def set_alarms(self, weekday, single, prealarm, snooze,
                   maxtemp, mintemp, rain, wind, gust):
        buf = self._read_alarms()
        if weekday.lower() != 'off':
            weekday_list = weekday.split(':')
            buf[0] = rev_int2bcd(int(weekday_list[0])) | 0x4
            buf[1] = rev_int2bcd(int(weekday_list[1]))
        else:
            buf[0] &= 0xFB
        if single.lower() != 'off':
            single_list = single.split(':')
            buf[2] = rev_int2bcd(int(single_list[0]))
            buf[3] = rev_int2bcd(int(single_list[1]))
            buf[0] |= 0x8
        else:
            buf[0] &= 0xF7
        if (prealarm.lower() != 'off' and
            (weekday.lower() != 'off' or single.lower() != 'off')):
            if int(prealarm) == 15:
                buf[4] = 0x10
            elif int(prealarm) == 30:
                buf[4] = 0x20
            elif int(prealarm) == 45:
                buf[4] = 0x30
            elif int(prealarm) == 60:
                buf[4] = 0x40
            elif int(prealarm) == 90:
                buf[4] = 0x50
            buf[2] |= 0x8
        else:
            buf[2] &= 0xF7
        buf[4] = (buf[4] & 0xF0) + int(snooze)
        buf[5] = self._checksum(buf[0:5])

        buf[32] = int2bcd(int(abs(float(maxtemp)) * 10) % 100)
        buf[33] = int2bcd(int(abs(float(maxtemp)) / 10))
        if float(maxtemp) >= 0:
            buf[33] |= 0x80
        if (abs(float(maxtemp)) * 100) % 10 == 5:
            buf[33] |= 0x20
        buf[34] = int2bcd(int(abs(float(mintemp)) * 10) % 100)
        buf[35] = int2bcd(int(abs(float(mintemp)) / 10))
        if float(mintemp) >= 0:
            buf[35] |= 0x80
        if (abs(float(mintemp)) * 100) % 10 == 5:
            buf[35] |= 0x20
        buf[36] = self._checksum(buf[32:36])

        if rain.lower() != 'off':
            buf[65] = int2bcd(int(rain) % 100)
            buf[66] = int2bcd(int(int(rain) / 100))
            buf[64] |= 0x4
        else:
            buf[64] = buf[64] & 0xFB
        if wind.lower() != 'off':
            buf[68] = int2bcd(int(float(wind) * 10) % 100)
            buf[69] = int2bcd(int(float(wind) / 10))
            buf[64] |= 0x2
        else:
            buf[64] = buf[64] & 0xFD
        if gust.lower() != 'off':
            buf[71] = int2bcd(int(float(gust) * 10) % 100)
            buf[72] = int2bcd(int(float(gust) / 10))
            buf[64] |= 0x1
        else:
            buf[64] |= 0xFE
        buf[73] = self._checksum(buf[64:73])
        self._write_alarms(buf)

    def get_interval(self):
        buf = self._read(0xFE)
        return buf[1]

    def get_interval_seconds(self):
        idx = self.get_interval()
        interval = self.idx_to_interval_sec.get(idx)
        if interval is None:
            msg = "Unrecognized archive interval '%s'" % idx
            log.error(msg)
            raise weewx.WeeWxIOError(msg)
        return interval

    def set_interval(self, idx):
        buf = self._read(0xFE)
        buf = buf[1:33]
        buf[0] = idx
        self._write(0xFE, buf)

    @staticmethod
    def _checksum(buf):
        crc = 0x100
        for i in range(len(buf)):
            crc -= buf[i]
            if crc < 0:
                crc += 0x100
        return crc


# define a main entry point for basic testing of the station without weewx
# engine and service overhead.  invoke this as follows from the weewx root dir:
#
# PYTHONPATH=bin python bin/weewx/drivers/te923.py
#
# by default, output matches that of te923tool
#    te923con                 display current weather readings
#    te923con -d              dump 208 memory records
#    te923con -s              display station status
#
# date; PYTHONPATH=bin python bin/user/te923.py --records 0 > c; date
# 91s
# Thu Dec 10 00:12:59 EST 2015
# Thu Dec 10 00:14:30 EST 2015
# date; PYTHONPATH=bin python bin/weewx/drivers/te923.py --records 0 > b; date
# 531s
# Tue Nov 26 10:37:36 EST 2013
# Tue Nov 26 10:46:27 EST 2013
# date; /home/mwall/src/te923tool-0.6.1/te923con -d > a; date
# 53s
# Tue Nov 26 10:46:52 EST 2013
# Tue Nov 26 10:47:45 EST 2013

if __name__ == '__main__':
    import optparse

    import weewx
    import weeutil.logger

    FMT_TE923TOOL = 'te923tool'
    FMT_DICT = 'dict'
    FMT_TABLE = 'table'

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

    def main():
        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',
                          help='display diagnostic information while running')
        parser.add_option('--status', dest='status', action='store_true',
                          help='display station status')
        parser.add_option('--readings', dest='readings', action='store_true',
                          help='display sensor readings')
        parser.add_option("--records", dest="records", type=int, metavar="N",
                          help="display N station records, oldest to newest")
        parser.add_option('--blocks', dest='blocks', type=int, metavar="N",
                          help='display N 32-byte blocks of station memory')
        parser.add_option("--format", dest="format", type=str,metavar="FORMAT",
                          default=FMT_TE923TOOL,
                          help="format for output: te923tool, table, or dict")
        (options, _) = parser.parse_args()

        if options.version:
            print("te923 driver version %s" % DRIVER_VERSION)
            exit(1)

        if options.debug:
            weewx.debug = 1

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

        if (options.format.lower() != FMT_TE923TOOL and
            options.format.lower() != FMT_TABLE and
            options.format.lower() != FMT_DICT):
            print("Unknown format '%s'.  Known formats include: %s" % (
                options.format, ','.join([FMT_TE923TOOL, FMT_TABLE, FMT_DICT])))
            exit(1)

        with TE923Station() as station:
            if options.status:
                data = station.get_versions()
                data.update(station.get_status())
                if options.format.lower() == FMT_TE923TOOL:
                    print_status(data)
                else:
                    print_data(data, options.format)
            if options.readings:
                data = station.get_readings()
                if options.format.lower() == FMT_TE923TOOL:
                    print_readings(data)
                else:
                    print_data(data, options.format)
            if options.records is not None:
                for data in station.gen_records(requested=options.records):
                    if options.format.lower() == FMT_TE923TOOL:
                        print_readings(data)
                    else:
                        print_data(data, options.format)
            if options.blocks is not None:
                for ptr, block in station.gen_blocks(count=options.blocks):
                    print_hex(ptr, block)

    def print_data(data, fmt):
        if fmt.lower() == FMT_TABLE:
            print_table(data)
        else:
            print(data)

    def print_hex(ptr, data):
        print("0x%06x %s" % (ptr, _fmt(data)))

    def print_table(data):
        """output entire dictionary contents in two columns"""
        for key in sorted(data):
            print("%s: %s" % (key.rjust(16), data[key]))

    def print_status(data):
        """output status fields in te923tool format"""
        print("0x%x:0x%x:0x%x:0x%x:0x%x:%d:%d:%d:%d:%d:%d:%d:%d" % (
            data['version_sys'], data['version_bar'], data['version_uv'],
            data['version_rcc'], data['version_wind'],
            data['bat_rain'], data['bat_uv'], data['bat_wind'], data['bat_5'],
            data['bat_4'], data['bat_3'], data['bat_2'], data['bat_1']))

    def print_readings(data):
        """output sensor readings in te923tool format"""
        output = [str(data['dateTime'])]
        output.append(getvalue(data, 't_in', '%0.2f'))
        output.append(getvalue(data, 'h_in', '%d'))
        for i in range(1, 6):
            output.append(getvalue(data, 't_%d' % i, '%0.2f'))
            output.append(getvalue(data, 'h_%d' % i, '%d'))
        output.append(getvalue(data, 'slp', '%0.1f'))
        output.append(getvalue(data, 'uv', '%0.1f'))
        output.append(getvalue(data, 'forecast', '%d'))
        output.append(getvalue(data, 'storm', '%d'))
        output.append(getvalue(data, 'winddir', '%d'))
        output.append(getvalue(data, 'windspeed', '%0.1f'))
        output.append(getvalue(data, 'windgust', '%0.1f'))
        output.append(getvalue(data, 'windchill', '%0.1f'))
        output.append(getvalue(data, 'rain', '%d'))
        print(':'.join(output))

    def getvalue(data, label, fmt):
        if label + '_state' in data:
            if data[label + '_state'] == STATE_OK:
                return fmt % data[label]
            else:
                return data[label + '_state']
        else:
            if data[label] is None:
                return 'x'
            else:
                return fmt % data[label]

if __name__ == '__main__':
    main()
