## Copyright 2010, 2011 Chris Schlaeger <chris@linux.com>
##
## This WMR200 driver was modeled after the WMRS200 driver. It contains
## some code fragment from the original WMRS200 file. There portions are
##
## Copyright 2009 Jordi Puigsegur <jordi.puigsegur@gmail.com>
##                Laurent Bovet <laurent.bovet@windmaster.ch>
##
##  This file is part of WFrog
##
##  WFrog is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program.  If not, see <http://www.gnu.org/licenses/>.

# There is no known official documentation for the USB protocol that
# the WMR200 uses to exchange weather data with a PC. Too bad that
# Oregon Scientific does not understand the benefits of open
# protocols. The code in this WMR200 driver is based on a collective
# reverse engineering effort. Most of the decoding is probably
# accurate, but minor bugs cannot be ruled out. Please submit a bug
# report at http://code.google.com/p/wfrog/issues/list in case you see
# odd behaviour. The report must include the full debug output of
# the logger.
#
#    cd wflogger
#    ./wflogger -d 2> wmr200.log
#
# Attach the wmr200.log file and include the corresponding actual
# readings from your WMR200 display.
# I'd like to thank the folks at
# http://aguilmard.com/phpBB3/viewtopic.php?f=2&t=508&st=0&sk=t&sd=a&sid=4f64fc06860272367eb6c9e408acabe1
# for their previous work. Also, the work from Denis Ducret
# <info@windspots.com> was very helpful to write this driver. His data
# logger can be found at http://www.sdic.ch/innovation/contributions.
# Probably the most comprehensive WRM200 protocol description was compiled by
# Rainer Finkeldeh and can be found at http://www.bashewa.com/wmr200-protocol.php.
# Without the diligent reverse engineering efforts of these folks, this driver
# would not have been possible.

from base import BaseStation
import time
import datetime
import logging
import threading
import platform
import sys

windDirMap = { 0:"N", 1:"NNE", 2:"NE", 3:"ENE",
               4:"E", 5:"ESE", 6:"SE", 7:"SSE",
               8:"S", 9:"SSW", 10:"SW", 11:"WSW",
               12:"W", 13:"WNW", 14:"NW", 15:"NNW" }
forecastMap = { 0:"Partly Cloudy", 1:"Rainy", 2:"Cloudy", 3:"Sunny",
                4:"Clear Night", 5:"Snowy",
                6:"Partly Cloudy Night", 7:"Unknown7" }
trends =      { 0:"Stable", 1:"Rising", 2:"Falling", 3:"Undefined" }

usbWait = 0.5
usbTimeout = 3.0

# The USB vendor and product ID of the WMR200. Unfortunately, Oregon
# Scientific is using this combination for several other products as
# well. Checking for it, is not good enough to reliable identify the
# WMR200.
vendor_id  = 0xfde
product_id = 0xca01

def detect():
  station = WMR200Station()
  station.init()
  if station.connectDevice(silent_fail=True) is not None:
    return station

class WMR200Error(IOError):
  "Used to signal an error condition"

class WMR200Station(BaseStation):
    '''
    Station driver for the Oregon Scientific WMR200.
    '''

    logger = logging.getLogger('station.wmr200')

    name = "Oregon Scientific WMR200"

    def init(self):
      # The delay between data requests. This value will be adjusted
      # automatically.
      self.pollDelay = 2.5
      # Initialize some statistic counters.
      # The total number of packets.
      self.totalPackets = 0
      # The number of correctly received USB packets.
      self.packets = 0
      # The total number of received data frames.
      self.frames = 0
      # The number of corrupted packets.
      self.badPackets = 0
      # The number of corrupted frames
      self.badFrames = 0
      # The number of checksum errors
      self.checkSumErrors = 0
      # The number of sent requests for data frames
      self.requests = 0
      # The number of USB connection resyncs
      self.resyncs = 0
      # The time when the program was started
      self.start = time.time()
      # The time of the last resync start or end
      self.lastResync = time.time()
      # True if we are (re-)synching with the station
      self.syncing = True
      # The accumulated time in logging mode
      self.loggedTime = 0
      # Difference between the PC clock and the station clock in
      # minutes.
      self.clockDelta = 0
      # The accumulated time in (re-)sync mode
      self.resyncTime = 0
      # Counters for each of the differnt data record types (0xD1 -
      # 0xD9)
      self.recordCounters = [ 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
      self.devh = None

    def _list2bytes(self, d):
        return reduce(lambda a, b: a + b, map(lambda a: "%02X " % a, d))

    def searchDevice(self, vendorId, productId):
      try:
        import usb
      except Exception, e:
        self.logger.warning(e)
        return None
      busses = usb.busses()
      for bus in busses:
        for device in bus.devices:
          if device.idVendor == vendorId and device.idProduct == productId:
            self.usbDevice = device
            self.usbConfiguration = device.configurations[0]
            self.usbInterface = self.usbConfiguration.interfaces[0][0]
            return device

    # After each 0xD0 command, the station will provide a set of data
    # packets. The first byte of each packet indicates the number of
    # valid octects in the packet. The length octect is not counted,
    # so the maximum value for the first octet is 7. The remaining
    # octets to fill the 8 octests are invalid. The actual weather
    # data is contained in data frames that may spread over several
    # packets. If the read times-out, we have received the final
    # packet of the last frame.
    def receivePacket(self):
      import usb
      while True:
        try:
          packet = self.devh.interruptRead(usb.ENDPOINT_IN + 1, 8,
                                           int(self.pollDelay * 1000))
          self.totalPackets += 1

          # Provide some statistics on the USB connection every 1000
          # packets.
          if self.totalPackets > 0 and self.totalPackets % 1000 == 0:
            self.logStats()

          if len(packet) != 8:
            # Valid packets must always have 8 octets.
            self.badPackets += 1
            self.logger.error("Wrong packet size: %s" %
                              self._list2bytes(packet))
          elif packet[0] > 7:
            # The first octet is always the number of valid octets in the
            # packet. Since a packet can only have 8 bytes, ignore all packets
            # with a larger size value. It must be corrupted.
            self.badPackets += 1
            self.logger.error("Bad packet: %s" % self._list2bytes(packet))
          else:
            # We've received a new packet.
            self.packets += 1
            self.logger.debug("Packet: %s" % self._list2bytes(packet))
            return packet

        except usb.USBError, e:
          self.logger.debug("Exception reading interrupt: "+ str(e))
          return None

    def sendPacket(self, packet):
      import usb
      try:
        self.devh.controlMsg(usb.TYPE_CLASS + usb.RECIP_INTERFACE,
                             0x9, packet, 0x200,
                             timeout = int(usbTimeout * 1000))
      except usb.USBError, e:
        if e.args != ('No error',):
          self.logger.exception("Can't write request record: "+ str(e))

    # The WMR200 is known to support the following commands:
    # 0xD0: Request next set of data frames.
    # 0xDA: Check if station is ready.
    # 0xDB: Clear historic data from station memory.
    # 0xDF: Not really known. Maybe to signal end of data transfer.
    def sendCommand(self, command):
      # All commands are only 1 octect long.
      self.sendPacket([0x01, command, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
      self.logger.debug("Command: %02X" % command)

    def clearReceiver(self):
      while True:
        if self.receivePacket() == None:
          break

    def connectDevice(self, silent_fail=False):
      import usb

      if silent_fail:
        self.logger.debug("USB initialization")
      else:
        self.logger.info("USB initialization")
      self.syncMode(True)
      self.resyncs += 1
      try:
        dev = self.searchDevice(vendor_id, product_id)

        if dev == None:
          raise WMR200Error("USB WMR200 not found (%04X %04X)" %
                            (vendor_id, product_id))

        self.devh = dev.open()
        self.logger.info("Oregon Scientific weather station found")
        self.logger.info("Manufacturer: %s" % dev.iManufacturer)
        self.logger.info("Product: %s" % dev.iProduct)
        self.logger.info("Device version: %s" % dev.deviceVersion)
        self.logger.info("USB version: %s" % dev.usbVersion)

        try:
          self.devh.detachKernelDriver(self.usbInterface.interfaceNumber)
          self.logger.info("Unloaded other driver from interface %d" %
              self.usbInterface.interfaceNumber)
        except usb.USBError, e:
          pass

        # The following init sequence was adapted from Denis Ducret's
        # wmr200log program.
        self.devh.setConfiguration(self.usbConfiguration)
        self.devh.claimInterface(self.usbInterface)
        self.devh.setAltInterface(self.usbInterface)
        self.devh.reset()
        time.sleep(usbWait)

        # WMR200 Init sequence
        self.logger.debug("Sending 0xA message")
        self.devh.controlMsg(usb.TYPE_CLASS + usb.RECIP_INTERFACE,
                             0xA, [], 0, 0, int(usbTimeout * 1000))
        time.sleep(usbWait)
        self.devh.getDescriptor(0x22, 0, 0x62)
        # Ignore any response packets the commands might have generated.
        self.clearReceiver()

        self.logger.debug("Sending init message")
        self.sendPacket([0x20, 0x00, 0x08, 0x01, 0x00, 0x00, 0x00, 0x00])
        # Ignore any response packets the commands might have generated.
        self.clearReceiver()


        # This command clears the WMR200 history memory. We can use it
        # if we don't care about old data that has been logged to the
        # station memory.
        # self.sendCommand(0xDB)
        # self.clearReceiver()

        # This command is supposed to stop the communication between
        # PC and the station.
        self.sendCommand(0xDF)
        # Ignore any response packets the commands might have generated.
        self.clearReceiver()

        # This command is a 'hello' command. The station respons with
        # a 0x01 0xD1 packet.
        self.sendCommand(0xDA)
        packet = self.receivePacket()
        if packet == None:
          self.logger.error("Station did not respond properly to WMR200 ping")
          return None
        elif packet[0] == 0x01 and packet[1] == 0xD1:
          self.logger.info("Station identified as WMR200")
        else:
          self.logger.error("Ping answer doesn't match: %s" %
                            self._list2bytes(packet))
          return None

        self.clearReceiver()
        self.logger.info("USB connection established")

        return self.devh
      except usb.USBError, e:
        if silent_fail:
           self.logger.debug("WMR200 connect failed: %s" % str(e))
        else:
            self.logger.exception("WMR200 connect failed: %s" % str(e))
        self.disconnectDevice()
        return None

    def disconnectDevice(self):
      import usb

      if self.devh == None:
        return

      try:
        # Tell console the session is finished.
        self.sendCommand(0xDF)
        try:
          self.devh.releaseInterface()
        except ValueError:
          None
        self.logger.info("USB connection closed")
        time.sleep(usbTimeout)
      except usb.USBError, e:
        self.logger.exception("WMR200 disconnect failed: %s" % str(e))
      self.devh = None

    def increasePollDelay(self):
      if self.pollDelay < 5.0:
        self.pollDelay += 0.1
        self.logger.debug("Polling delay increased: %.1f" % self.pollDelay)

    def decreasePollDelay(self):
      if self.pollDelay > 0.5:
        self.pollDelay -= 0.1
        self.logger.debug("Polling delay decreased: %.1f" % self.pollDelay)

    def run(self, generate_event, send_event):
      # Initialize injected functions used by BaseStation
      self.generate_event = generate_event
      self.send_event = send_event
      self.logger.info("Thread started")
      self.init()

      while True:
        try:
          if self.devh == None:
            self.connectDevice()
          self.logData()
        except WMR200Error, e:
          self.logger.error("Re-syncing USB connection")
        self.disconnectDevice()

        self.logStats()
        # The more often we resync, the less likely we get back in
        # sync. To prevent useless retries, we wait longer the more
        # often we've tried.
        time.sleep(self.resyncs)

    # The weather data is contained in frames of variable length. The
    # first octet of each frame indicates the type of the frame. Valid
    # types are 0xD1 to 0xD9. The 0xD1 frame is only 1 octet long. It
    # is sent as a response to a 0xDA command. The 0xD2 - 0xD9 frames
    # are responses to a 0xD0 command. 0xD8 frames are probably not
    # used. The meaning of 0xD9 frames is currently unknown.
    def receiveFrames(self):
      packets = []
      # Collect packets until we get no more data. By then we should have
      # received one or more frames.
      while True:
        packet = self.receivePacket()
        if packet == None:
          break
        # The first octet is the length. Only length octets are valid data.
        packets += packet[1:packet[0] + 1]

      frames = []
      while True:
        if len(packets) == 0:
          # There should be at least one frame.
          if len(frames) == 0:
            # If we get empty frames we increase the polling delay a
            # bit.
            self.increasePollDelay()
            return None
          # We've found all the frames in the packets.
          break

        self.frames += 1

        if packets[0] < 0xD1 or packets[0] > 0xD9:
          # All frames must start with 0xD1 - 0xD9. If the first byte is
          # not within this range, we don't have a proper frame start.
          # Discard all octets and restart with the next packet.
          self.logger.error("Bad frame: %s" % self._list2bytes(packets))
          self.badFrames += 1
          break

        if packets[0] == 0xD1 and len(packets) == 1:
          # 0xD1 frames have only 1 octet.
          frame = packets[0:1]
          packets = packets[1:len(packets)]
          frames.append(frame)
        elif len(packets) < 2 or len(packets) < packets[1]:
          # 0xD2 - 0xD9 frames use the 2nd octet to specifiy the length of the
          # frame. The length includes the type and length octet.
          self.logger.error("Short frame: %s" % self._list2bytes(packets))
          self.badFrames += 1
          break
        elif packets[1] < 8:
	  # All valid D2 - D9 frames must have at least a length of 8
          self.logger.error("Bad frame length: %d" % packets[1])
          self.badFrames += 1
        else:
          # This is for all frames with length byte and checksum.
          frame = packets[0:packets[1]]
          packets = packets[packets[1]:len(packets)]

          # The last 2 octets of D2 - D9 frames are always the low and high byte
          # of the checksum. We ignore all frames that don't have a matching
          # checksum.
          if self.checkSum(frame[0:len(frame)-2],
                           frame[len(frame) - 2] |
                           (frame[len(frame) - 1] << 8)) == False:
            self.checkSumErrors += 1
            break

          frames.append(frame)

      if len(frames) > 0:
        if len(frames) > 2:
          # If we get more than 2 frames at a time we increase the
          # polling frequency again.
          self.decreasePollDelay()
        return frames
      else:
        return None

    def logData(self):
      while True:
        # Requesting the next set of data frames by sending a D0
        # command.
        self.sendCommand(0xD0)
        self.requests += 1
        # Get the frames.
        frames = self.receiveFrames()
        if frames == None:
          # The station does not have any data right now. Just wait a
          # bit and ask again.
          time.sleep(usbTimeout)
        else:
          # Send the received frames to the decoder.
          for frame in frames:
            self.decodeFrame(frame)

    def decodeFrame(self, record):
      self.syncMode(False)
      self.logger.debug("Frame: %s" % self._list2bytes(record))
      type = record[0]
      self.recordCounters[type - 0xD1] += 1
      if type == 0xD2:
        self.logger.info(">>>>> Historic Data Record >>>>>")
        # We ignore 0xD2 frames for now. They only contain historic data.
        # Byte 2 - 6 contains the time stamp.
        timeStamp = self.decodeTimeStamp(record[2:7], '@Time', False)
        # Bytes 7 - 19 contain rain data
        rainTotal, rainRate = self.decodeRain(record[7:20])
        # Bytes 20 - 26 contain wind data
        dirDeg, avgSpeed, gustSpeed, windchill = self.decodeWind(record[20:27])
        # Byte 27 contains UV data
        uv = self.decodeUV(record[27])
        # Bytes 28 - 32 contain pressure data
        pressure = self.decodePressure(record[28:32])
        # Byte 32: number of external sensors
        externalSensors = record[32]
        # Bytes 33 - end contain temperature and humidity data
        data = self.decodeTempHumid(record[33:33 + (1 + externalSensors) * 7])
        self.logger.info("<<<<< End Historic Record <<<<<")

        # TODO: Find out how "no wind data" is encoded and ignore it.
        self._report_wind(dirDeg, avgSpeed, gustSpeed, timeStamp)
        # TODO: Find out how "no rain data" is encoded and ignore it.
        self._report_rain(rainTotal, rainRate, timeStamp)
        # If no UV data is present, the value is 0xFF.
        if uv != 0xFF:
          self._report_uv(uv, timeStamp)
        self._report_barometer_absolute(pressure, timeStamp)
        for d in data:
          temp, humidity, sensor = d
          self._report_temperature(temp, humidity, sensor, timeStamp)
      elif type == 0xD3:
        # 0xD3 frames contain wind related information.
        # Byte 2 - 6 contains the time stamp.
        self.decodeTimeStamp(record[2:7])
        dirDeg, avgSpeed, gustSpeed, windchill = self.decodeWind(record[7:15])
        self._report_wind(dirDeg, avgSpeed, gustSpeed)
      elif type == 0xD4:
        # 0xD4 frames contain rain data
        # Byte 2 - 6 contains the time stamp.
        self.decodeTimeStamp(record[2:7])
        rainTotal, rainRate = self.decodeRain(record[7:21])
        self._report_rain(rainTotal, rainRate)
      elif type == 0xD5:
        # 0xD5 frames contain UV data.
        # Untested. I don't have a UV sensor.
        # Byte 2 - 6 contains the time stamp.
        self.decodeTimeStamp(record[2:7])
        uv = self.decodeUV(record[7])
        self._report_uv(uv)
      elif type == 0xD6:
        # 0xD6 frames contain forecast and air pressure data.
        # Byte 2 - 6 contains the time stamp.
        self.decodeTimeStamp(record[2:7])
        pressure = self.decodePressure(record[7:11])
        self._report_barometer_absolute(pressure)
      elif type == 0xD7:
        # 0xD7 frames contain humidity and temperature data.
        # Byte 2 - 6 contains the time stamp.
        self.decodeTimeStamp(record[2:7])
        data = self.decodeTempHumid(record[7:14])
        temp, humidity, sensor = data[0]
        self._report_temperature(temp, humidity, sensor)
      elif type == 0xD8:
        # 0xD8 frames have never been observed.
        self.logger.info("TODO: 0xD8 frame found: %s" %
                         self._list2bytes(record))
      elif type == 0xD9:
        # 0x09 frames contain status information about the devices. The
        # meaning of several bits is still unknown. Maybe they are not in use.
        self.decodeStatus(record[2:8])

    def decodeTimeStamp(self, record, label = 'Time', check = True):
      minutes = record[0]
      hours = record[1]
      day = record[2]
      # The WMR200 can sometimes return all 0 bytes. We interpret this as
      # 2000-01-01 0:00
      if day == 0:
        day = 1
      month = record[3]
      if month == 0:
        month = 1
      year = 2000 + record[4]
      date = "%04d-%02d-%02d %d:%02d" % (year, month, day, hours, minutes)
      self.logger.info("%s: %s" % (label, date))
      ts = time.mktime((year, month, day, hours, minutes, 0, -1, -1, -1))

      if check:
        self.clockDelta = int(time.time() / 60) - int(ts / 60)

      return datetime.datetime(year, month, day, hours, minutes)

    def decodeWind(self, record):
      # Byte 0: Wind direction in steps of 22.5 degrees.
      # 0 is N, 1 is NNE and so on. See windDirMap for complete list.
      dirDeg = (record[0] & 0xF) * 22.5
      # Byte 1: Always 0x0C? Maybe high nible is high byte of gust speed.
      # Byts 2: The low byte of gust speed in 0.1 m/s.
      gustSpeed = ((((record[1] >> 4) & 0xF) << 8) | record[2]) * 0.1
      if record[1] != 0x0C:
        self.logger.info("TODO: Wind byte 1: %02X" % record[1])
      # Byte 3: High nibble seems to be low nibble of average speed.
      # Byte 4: Maybe low nibble of high byte and high nibble of low byte
      #          of average speed. Value is in 0.1 m/s
      avgSpeed = ((record[4] << 4) | ((record[3] >> 4) & 0xF)) * 0.1
      if (record[3] & 0x0F) != 0:
        self.logger.info("TODO: Wind byte 3: %02X" % record[3])
      # Byte 5 and 6: Low and high byte of windchill temperature. The value is
      # in 0.1F. If no windchill is available byte 5 is 0 and byte 6 0x20.
      # Looks like OS hasn't had their Mars Climate Orbiter experience yet.
      if record[5] != 0 or record[6] != 0x20:
        windchill = (((record[6] << 8) | record[5]) - 320) * (5.0 / 90.0)
      else:
        windchill = None

      self.logger.info("Wind Dir: %s" % windDirMap[record[0]])
      self.logger.info("Gust: %.1f m/s" % gustSpeed)
      self.logger.info("Wind: %.1f m/s" % avgSpeed)
      if windchill != None:
        self.logger.info("Windchill: %.1f C" % windchill)

      return (dirDeg, avgSpeed, gustSpeed, windchill)

    def decodeRain(self, record):
      # Bytes 0 and 1: high and low byte of the current rainfall rate
      # in 0.1 in/h
      rainRate = ((record[1] << 8) | record[0]) / 3.9370078
      # Bytes 2 and 3: high and low byte of the last hour rainfall in 0.1in
      rainHour = ((record[3] << 8) | record[2]) / 3.9370078
      # Bytes 4 and 5: high and low byte of the last day rainfall in 0.1in
      rainDay = ((record[5] << 8) | record[4]) / 3.9370078
      # Bytes 6 and 7: high and low byte of the total rainfall in 0.1in
      rainTotal = ((record[7] << 8) | record[6]) / 3.9370078

      self.logger.info("Rain Rate: %.1f mm/hr" % rainRate)
      self.logger.info("Rain Hour: %.1f mm" % rainHour)
      self.logger.info("Rain 24h: %.1f mm" % rainDay)
      self.logger.info("Rain Total: %.1f mm" % rainTotal)
      # Bytes 8 - 12 contain the time stamp since the measurement started.
      self.decodeTimeStamp(record[8:13], 'Since', False)
      return (rainTotal, rainRate)

    def decodeUV(self, uv):
      self.logger.info("UV Index: %d" % uv)
      return uv

    def decodePressure(self, record):
      # Byte 0: low byte of pressure. Value is in hPa.
      # Byte 1: high nibble is probably forecast
      #         low nibble is high byte of pressure.
      pressure = ((record[1] & 0xF) << 8) | record[0]
      forecast = forecastMap[(record[1] & 0x70) >> 4]
      # Bytes 2 - 3: Similar to bytes 0 and 1, but altitude corrected
      # pressure. Upper nibble of byte 3 is still unknown. Seems to
      # be always 3.
      altPressure = (record[3] & 0xF) * 256 + record[2]
      unknownNibble = (record[3] & 0x70) >> 4

      self.logger.info("Forecast: %s" % forecast)
      self.logger.info("Measured Pressure: %d hPa" % pressure)
      if unknownNibble != 3:
        self.logger.info("TODO: Pressure unknown nibble: %d" % unknownNibble)
      self.logger.info("Altitude corrected Pressure: %d hPa" % altPressure)
      return pressure

    def decodeTempHumid(self, record):
      data = []
      # The historic data can contain data from multiple sensors. I'm not
      # sure if the 0xD7 frames can do too. I've never seen a frame with
      # multiple sensors. But historic data bundles data for multiple
      # sensors.
      rSize = 7
      for i in xrange(len(record) / rSize):
        # Byte 0: low nibble contains sensor ID. 0 for base station.
        sensor = record[i * rSize] & 0xF
        tempTrend = (record[i * rSize] >> 6) & 0x3
        humTrend = (record[i * rSize] >> 4) & 0x3
        # Byte 1: probably the high nible contains the sign indicator.
        #         The low nibble is the high byte of the temperature.
        # Byte 2: The low byte of the temperature. The value is in 1/10
        # degrees centigrade.
        temp = (((record[i * rSize + 2] & 0x0F) << 8) +
                record[i * rSize + 1]) * 0.1
        if record[i * rSize + 2] & 0x80:
          temp = -temp
        # Byte 3: The humidity in percent.
        humidity = record[i * rSize + 3]
        # Bytes 4 and 5: Like bytes 1 and 2 but for dew point.
        dewPoint = (((record[i * rSize + 5] & 0x0F) << 8) +
                    record[i * rSize + 4]) * 0.1
        if record[i * rSize + 5] & 0x80:
          dewPoint = -dewPoint
        # Byte 6: Head index
        if record[i * rSize + 6] != 0:
          headIndex = (record[i * rSize + 6] - 32) / 1.8
        else:
          headIndex = None

        self.logger.info("Temperature %d: %.1f C  Trend: %s" %
                          (sensor, temp, trends[tempTrend]))
        self.logger.info("Humidity %d: %d%%   Trend: %s" %
                         (sensor, humidity, trends[humTrend]))
        self.logger.info("Dew point %d: %.1f C" % (sensor, dewPoint))
        if headIndex:
          self.logger.info("Heat index: %d" % (headIndex))

        data.append((temp, humidity, sensor))

      return data

    def decodeStatus(self, record):
      # Byte 0
      if record[0] & int('11111100', 2):
        self.logger.info("TODO: Unknown bits in D9 frame byte 0: %0x2X" %
                         (record[0]))
      if record[0] & 0x2:
        self.logger.warning("Sensor 1 fault (temp/hum outdoor)")
      if record[0] & 0x1:
        self.logger.warning("Wind sensor fault")

      # Byte 1
      if record[1] & int('11001111',2):
        self.logger.info("TODO: Unknown bits in D9 frame byte 1: %02X" %
                         (record[1]))
      if record[1] & 0x20:
        self.logger.warning("UV Sensor fault")
      if record[1] & 0x10:
        self.logger.warning("Rain sensor fault")

      # Byte 2
      if record[2] & int('01111100', 2):
        self.logger.info("TODO: Unknown bits in D9 frame byte 2: %02X" %
                         (record[2]))
      if record[2] & 0x80:
        self.logger.warning("Weak RF signal. Clock not synched")
      if record[2] & 0x02:
        self.logger.warning("Sensor 1: Battery low")
      if record[2] & 0x01:
        self.logger.warning("Wind sensor: Battery low")

      # Byte 3
      if record[3] & int('11001111', 2):
        self.logger.info("TODO: Unknown bits in D9 frame byte 3: %02X" %
                         (record[3]))
      if record[3] & 0x20:
        self.logger.warning("UV sensor: Battery low")
      if record[3] & 0x10:
        self.logger.warning("Rain sensor: Battery low")

    def checkSum(self, packet, checkSum):
      sum = 0
      for byte in packet:
        sum += byte
      if sum != checkSum:
        self.logger.error("Checksum error: %d instead of %d" % (sum, checkSum))
        return False
      return True

    def logStats(self):
      now = time.time()
      uptime = now - self.start
      self.logger.info("Uptime: %s" % self.durationToStr(uptime))
      if self.totalPackets > 0:
        self.logger.info("Total packets: %d" % self.totalPackets)
        self.logger.info("Good packets: %d (%.1f%%)" %
                          (self.packets,
                          self.packets * 100.0 / self.totalPackets))
        self.logger.info("Bad packets: %d (%.1f%%)" %
                         (self.badPackets,
                          self.badPackets * 100.0 / self.totalPackets))
      if self.frames > 0:
        self.logger.info("Frames: %d" % self.frames)
        self.logger.info("Bad frames: %d (%.1f%%)" %
                         (self.badFrames,
                          self.badFrames * 100.0 / self.frames))
        self.logger.info("Checksum errors: %d (%.1f%%)" %
                         (self.checkSumErrors,
                          self.checkSumErrors * 100.0 / self.frames))
        self.logger.info("Requests: %d" % self.requests)
      self.logger.info("Clock delta: %d" % self.clockDelta)
      # Generate a warning if PC and station clocks are more than 2
      # minutes out of sync.
      if abs(self.clockDelta) > 2:
        self.logger.warning("PC and station clocks are out of sync")

      self.logger.info("Polling delay: %.1f" % self.pollDelay)
      self.logger.info("USB resyncs: %d" % self.resyncs)

      loggedTime = self.loggedTime
      resyncTime = self.resyncTime
      if not self.syncing:
        loggedTime += now - self.lastResync
      else:
        resyncTime += now - self.lastResync
      self.logger.info("Logged time: %s (%.1f%%)" %
                       (self.durationToStr(loggedTime),
                        loggedTime * 100.0 / uptime))
      self.logger.info("Resync time: %s (%.1f%%)" %
                       (self.durationToStr(resyncTime),
                        resyncTime * 100.0 / uptime))
      if self.frames > 0:
        for i in xrange(9):
          self.logger.info("0x%X records: %8d (%2d%%)" %
                           (0xD1 + i, self.recordCounters[i],
                            self.recordCounters[i] * 100.0 / self.frames))

    def durationToStr(self, sec):
      seconds = sec % 60
      minutes = (sec / 60) % 60
      hours = (sec / (60 * 60)) % 24
      days = (sec / (60 * 60 * 24))
      return ("%d days, %d hours, %d minutes, %d seconds" %
              (days, hours, minutes, seconds))

    def syncMode(self, on):
      now = time.time()
      if self.syncing:
        if not on:
          self.logger.info("*** Switching to log mode ***")
          # We are in sync mode and need to switch to log mode now.
          self.resyncTime += now - self.lastResync
          self.lastResync = now
          self.syncing = False
      else:
        if on:
          self.logger.info("*** Switching to sync mode ***")
          # We are in log mode and need to switch to sync mode now.
          self.loggedTime += now - self.lastResync
          self.lastResync = now
          self.syncing = True

name = WMR200Station.name