# Copyright (c) 2013 Chris Manton <cmanton@gmail.com>  www.onesockoff.org
# See the file LICENSE.txt for your full rights.
#
# Special recognition to Lars de Bruin <l...@larsdebruin.net> for contributing
# packet decoding code.
#
# pylint parameters
# suppress global variable warnings
#   pylint: disable-msg=W0603
# suppress weewx driver methods not implemented
#   pylint: disable-msg=W0223  
# suppress weewx driver methods non-conforming name
#   pylint: disable-msg=C0103
# suppress too many lines in module
#   pylint: disable-msg=C0302
# suppress too many instance attributes
#   pylint: disable-msg=R0902
# suppress too many public methods
#   pylint: disable-msg=R0904
# suppress too many statements
#   pylint: disable-msg=R0915
# suppress unused arguments   e.g. loader(...,engine)
#   pylint: disable-msg=W0613
"""Classes and functions to interface with an Oregon Scientific WMR200 station

WMR200:
 - logger
 - up to 10 channels

Oregon Scientific
  http://us.oregonscientific.com/ulimages/manuals2/WMR200.pdf

Bronberg Weather Station
  For a pretty good summary of what's in these packets see
  http://www.bashewa.com/wmr200-protocol.php

The WMR200 does not report wind gust direction. 
"""

import select
import socket
import syslog
import threading
import time
import usb

import weewx.drivers
import weeutil.weeutil

DRIVER_NAME = 'WMR200'
DRIVER_VERSION = "3.3.2"


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

def confeditor_loader():
    return WMR200ConfEditor()


# General decoding sensor maps.
WIND_DIR_MAP = {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'}
FORECAST_MAP = {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'}

# Size of USB frame to read from weather console.
_WMR200_USB_FRAME_SIZE = 8

# Time to sleep in seconds between querying usb device thread
# for data.  This should be non-zero and reduces load on the machine.
_WMR200_USB_POLL_INTERVAL = 1

# Time interval in secs to send data to the wmr200 to request live data.
_WMR200_REQUEST_LIVE_DATA_INTERVAL = 30

# Time in secs to block and wait for data from the weather console device.
# Related to time to request live data.
_WMR200_USB_READ_DATA_INTERVAL = _WMR200_REQUEST_LIVE_DATA_INTERVAL / 2

# Time in ms to wait for USB reset to complete.
_WMR200_USB_RESET_TIMEOUT = 1000

# Guessed wmr200 protocol max packet size in bytes.
# This is only a screen to differentiate between good and
# bad packets.
_WMR200_MAX_PACKET_SIZE = 0x80

# Driver name.
_WMR200_DRIVER_NAME = 'wmr200'

# weewx configurable flags for enabling/disabling debug verbosity.
# Prints processed packets with context from console.
DEBUG_PACKETS_COOKED = 0
# Prints raw pre-processed packets from console.
DEBUG_PACKETS_RAW = 0
# Prints respective packets individually.
DEBUG_PACKETS_ARCHIVE = 0
DEBUG_PACKETS_PRESSURE = 0
DEBUG_PACKETS_RAIN = 0

DEBUG_PACKETS_STATUS = 0
DEBUG_PACKETS_TEMP = 0
DEBUG_PACKETS
_UVI = 0
DEBUG_PACKETS_WIND = 0
# Print communication messages 
DEBUG_COMM = 0
# Print weather station configuration.
DEBUG_CONFIG_DATA = 0
# Print all writes to weather console.
DEBUG_WRITES = 0
DEBUG_READS = 0
DEBUG_CHECKSUM = 0

def logmsg(dst, msg):
    """Base syslog helper"""
    syslog.syslog(dst, ('%s: %s: %s' %
                        (_WMR200_DRIVER_NAME,
                         threading.currentThread().getName(), msg)))

def logdbg(msg):
    """Debug syslog helper"""
    logmsg(syslog.LOG_DEBUG, 'D ' + msg)

def loginf(msg):
    """Info syslog helper"""
    logmsg(syslog.LOG_INFO, 'I ' + msg)

def logwar(msg):
    """Warning syslog helper"""
    logmsg(syslog.LOG_WARNING, 'W ' + msg)

def logerr(msg):
    """Error syslog helper"""
    logmsg(syslog.LOG_ERR, 'E ' + msg)

def logcrt(msg):
    """Critical syslog helper"""
    logmsg(syslog.LOG_CRIT, 'C ' + msg)


class WMR200PacketParsingError(Exception):
    """A driver handled recoverable packet parsing error condition."""
    def __init__(self, msg):
        super(WMR200PacketParsingError, self).__init__()
        self._msg = msg

    @property
    def msg(self):
        """Exception message to be logged to console."""
        return self._msg


class WMR200ProtocolError(weewx.WeeWxIOError):
    """Used to signal a protocol error condition"""
    def __init__(self, msg):
        super(WMR200ProtocolError, self).__init__()
        self._msg = msg 
        logerr(msg)


class UsbDevice(object):
    """General class to handles all access to device via USB bus."""
    def __init__(self):
        # Polling read timeout.
        self.timeout_read = _WMR200_USB_READ_DATA_INTERVAL
        # USB device used for libusb
        self.dev = None
        # Holds device handle for access
        self.handle = None
        # debug byte count
        self.byte_cnt_rd = 0
        self.byte_cnt_wr = 0
        # default to a sane endpoint
        self.in_endpoint = usb.ENDPOINT_IN + 1
        # only one interface
        self.interface = 0

    def find_device(self, vendor_id, product_id):
        """Find the given vendor and product IDs on the USB bus

        Returns: True if specified device was found, otherwise false.  """
        for bus in usb.busses():
            for dev in bus.devices:
                if dev.idVendor == vendor_id \
                   and dev.idProduct == product_id:
                    self.dev = dev
                    return True
        return False

    def open_device(self):
        """Opens a USB device and get a handle to read and write.
       
        A specific device must have been found."""
        try:
            self.handle = self.dev.open()
        except usb.USBError, exception:
            logcrt(('open_device() Unable to open USB interface.'
                    ' Reason: %s' % exception))
            raise weewx.WakeupError(exception)
        except AttributeError, exception:
            logcrt('open_device() Device not specified.')
            raise weewx.WakeupError(exception)

        # Detach any old claimed interfaces
        try:
            self.handle.detachKernelDriver(self.interface)
        except usb.USBError:
            pass

        try:
            self.handle.claimInterface(self.interface)
        except usb.USBError, exception:
            logcrt(('open_device() Unable to'
                    ' claim USB interface. Reason: %s' % exception))
            raise weewx.WakeupError(exception)

    def close_device(self):
        """Close a device for access.

        NOTE(CMM) There is no busses[].devices[].close() so under linux the
        file descriptor will remain open for the life of the process.
        An OS independant mechanism is required so 'lsof' and friends will
        not be cross platform."""
        try:
            self.handle.releaseInterface()
        except usb.USBError, exception:
            logcrt('close_device() Unable to'
                   ' release device interface. Reason: %s' % exception)

    def read_device(self):
        """Read a stream of data bytes from the device.
        
        Returns a list of valid protocol bytes from the device.
       
        The first byte indicates the number of valid bytes following
        the first byte that are valid protocol bytes.  Only the valid
        protocol bytes are returned.  """
        if not self.handle:
            msg = 'read_device() No USB handle for usb_device Read'
            logerr(msg)
            raise weewx.WeeWxIOError(msg)

        report = None
        try:
            report = self.handle.interruptRead(self.in_endpoint,
                                               _WMR200_USB_FRAME_SIZE,
                                               int(self.timeout_read) * 1000)

            # I think this value indicates that the buffer has overflowed.
            if report[0] == 8:
                msg = 'USB read_device overflow error'
       