#!/usr/bin/env python
"""
gw1000.py

A WeeWX driver for the Ecowitt GW1000 Wi-Fi Gateway API.

The WeeWX GW1000 driver utilise the GW1000 API thus using a pull methodology for
obtaining data from the GW1000 rather than the push methodology used by current
drivers. This has the advantage of giving the user more control over when the
data is obtained from the GW1000 plus also giving access to a greater range of
metrics.

The GW1000 driver can be operated as a traditional WeeWX driver where it is the
source of loop data or it can be operated as a WeeWX service where it is used
to augment loop data produced by another driver.

Copyright (C) 2020 Gary Roderick                   gjroderick<at>gmail.com

This program 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/.

Version: 0.1.0b5                                  Date: 24 July 2020

Revision History
    ?? ????? 2020      v0.1.0
        - initial release


Before running WeeWX with the GW1000 driver you may wish to run the driver from
the command line to ensure correct operation/assist in configuration. To run the
driver from the command line enter one of the the following commands depending
on your WeeWX installation type:

    for a setup.py install:

        $ PYTHONPATH=/home/weewx/bin python -m user.gw1000 --help

    or for package installs use:

        $ PYTHONPATH=/usr/share/weewx python -m user.gw1000 --help

Note. Whilst the driver may be run independently of WeeWX the driver still
requires WeeWX and it's dependencies be installed. Consequently, if WeeWX 4.0.0
or later is installed the driver must be run under the same Python version as
WeeWX uses. This means that on some systems 'python' in the above commands may
need to be changed to 'python2' or 'python3'.

Note. The nature of the GW1000 API and the GW1000 driver mean that the GW1000
driver can be run from the command line while the GW1000 continues to serve
data to any existing services. This makes it possible to configure and test the
GW1000 driver without taking the GW1000 off-line.

The --discover command line option is useful for discovering any GW1000 on the
local network. The IP address and port details returned by --discover can be
useful for configuring the driver IP address and port config options in
weewx.conf.

The --live-data command line option is useful for seeing what data is available
from the GW1000. Note the fields available will depend on the sensors connected
to the GW1000. As the field names returned by --live-data used are GW1000 field
names before mapping to WeeWX fields names, the --live-data output is useful for
configuring the field map to be used by the GW1000 driver.

Once you believe the GW1000 driver is configured the --test-driver or
--test-service command line options can be used to confirm correct operation of
the GW1000 driver as a driver or as a service respectively.

To use the GW1000 driver as a WeeWX driver:

1.  If installing on a fresh WeeWX installation install WeeWX and configure it
to use the 'simulator'. Refer to http://weewx.com/docs/usersguide.htm#installing

2.  If installing the driver using the wee_extension utility (the recommended
method):

    -   download the GW1000 driver extension package:

        $ wget -P /var/tmp https://github.com/gjr80/weewx-gw1000/releases/download/v0.1.0/gw1000-0.1.0.tar.gz

    -   install the GW1000 driver extension:

        $ wee_extension --install=/var/tmp/gw1000-0.1.0.tar.gz

    -   skip to step 4

3.  If installing manually:

    -   put this file in $BIN_ROOT/user.

    -   add the following stanza to weewx.conf:

[GW1000]
    # This section is for the GW1000

    # The driver itself
    driver = user.gw1000

    -   add the following stanza to weewx.conf:

    Note: If an [Accumulator] stanza already exists in weewx.conf just add the
          child settings.

[Accumulator]
    [[lightning_strike_count]]
        extractor = sum
    [[lightning_last_det_time]]
        extractor = last

4.  The GW1000 driver uses a default field map to map GW1000 API fields to
common WeeWX fields. If required this default field map can be overridden by
adding a [[field_map]] stanza to the [GW1000] stanza in weewx.conf. To override
the default sensor map add the following under the [GW1000] stanza in
weewx.conf altering/removing/adding field maps entries as required:

[GW1000]
    ...
    # Define a mapping to map GW1000 fields to WeeWX fields.
    #
    # Format is weewx_field = GW1000_field
    #
    # where:
    #   weewx_field is a WeeWX field name to be included in the generated loop
    #       packet
    #   GW1000_field is a GW1000 API field
    #
    #   Note: WeeWX field names will be used to populate the generated loop
    #         packets. Fields will only be saved to database if the field name
    #         is included in the in-use database schema.
    #
    [[field_map]]
       outTemp = outtemp
       ...

    Details of all supported GW1000 fields can be viewed by running the GW1000
    driver with the --default-map to display the default field map.

    However, the available GW1000 fields will depend on what sensors are
    connected to the GW1000. The available fields and current observation
    values for a given GW1000 can be viewed by running the GW1000 driver
    directly with the --live-data command line option.

5.  The default field map can also be modified without needing to specify the
entire field map by adding a [[field_map_extensions]] stanza to the [GW1000]
stanza in weewx.conf. The field mappings under [[field_map_extensions]] are
used to modify the default field map, for example, the following could be used
to map the humidity reading from WH31 channel 5 to the WeeWX inHumidity field
whilst keeping all other field mappings as is:

[GW1000]
    ...
    [[field_map_extensions]]
        inHumidity = humid5

6.  Test the now configured GW1000 driver using the --test-driver command line
option. You should observe loop packets being emitted on a regular basis using
the WeeWX field names from the default or modified field map.

7.  Configure the driver:

    $ sudo wee_config --reconfigure --driver=user.gw1000

6.  You may chose to run WeeWX directly to observe the loop packets and archive
records being generated by WeeWX. Refer to
http://weewx.com/docs/usersguide.htm#Running_directly.

6.  Once satisfied that the GW1000 driver is operating correctly you can start
the WeeWX daemon:

    $ sudo /etc/init.d/weewx start

    or

    $ sudo service weewx start

    or

    $ sudo systemctl start weewx

To use the GW1000 driver as a WeeWX service:

1.  Install WeeWX and configure it to use either the 'simulator' or another
driver of your choice. Refer to http://weewx.com/docs/usersguide.htm#installing.

2.  Install the GW1000 driver using the wee_extension utility as per 'To use the
GW1000 driver as a WeeWX driver' step 3 above or copy this file to
$BIN_ROOT/user.

3.  Modify weewx.conf as per 'To use the GW1000 driver as a WeeWX driver' step 3
above.

4.  Under the [Engine] [[Services]] stanza in weewx.conf and add an entry
'user.gw1000.Gw1000Service' to the data_services option. It should look
something like:

[Engine]

    [[Services]]
        ....
        data_services = user.gw1000.Gw1000Service

5.  If required, modify the default field map to suit as per 'To use the GW1000
driver as a WeeWX driver' steps 4 and 5.

6.  Test the now configured GW1000 service using the --test-service command line
option. You should observe loop packets being emitted on a regular basis that
include GW1000 data. Note that not all loop packets will include GW1000 data.

7.  You may chose to run WeeWX directly to observe the loop packets and archive
records being generated by WeeWX. Refer to
http://weewx.com/docs/usersguide.htm#Running_directly. Note that depending on
the frequency of the loop packets emitted by the in-use driver and the polling
interval of the GW1000 service not all loop packets may include GW1000 data.

8.  Once satisfied that the GW1000 service is operating correctly you can start
the WeeWX daemon:

    $ sudo /etc/init.d/weewx start

    or

    $ sudo service weewx start

    or

    $ sudo systemctl start weewx
"""
# TODO. Review against latest
# TODO. Confirm WH26/WH32 sensor ID
# TODO. Confirm sensor ID signal value meaning
# TODO. Confirm sensor ID battery meaning
# TODO. Confirm WH26/WH32 battery status
# TODO. Confirm WH51 battery status
# TODO. Confirm WH57 battery status and meaning
# TODO. Confirm WS68 battery status
# TODO. Confirm WS80 battery status
# TODO. Confirm WH41 battery status and meaning
# TODO. Confirm WH55 battery status
# TODO. Confirm WH24 battery status
# TODO. Confirm WH25 battery status
# TODO. Confirm WH40 battery status
# TODO. --sensors battery data does not agree with --live-data battery states (at least for WH57)
# TODO. Need to know date-time data format for decode date_time()
# TODO. Need to add battery status fields to field map
# TODO. Confirm frequency byte meaning for other than 433MHz
# TODO. Verify field map/field map extensions work correctly

# Python imports
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import re
import socket
import struct
import threading
import time
from operator import itemgetter

# python 2/3 compatibility shim
import six

# WeeWX imports
import weecfg
import weeutil.weeutil
import weewx.drivers
import weewx.engine
import weewx.wxformulas

# import/setup logging, WeeWX v3 is syslog based but WeeWX v4 is logging based,
# try v4 logging and if it fails use v3 logging
try:
    # WeeWX4 logging
    import logging
    from weeutil.logger import log_traceback
    log = logging.getLogger("%s: %s" % ('gw1000', __name__))

    def logdbg(msg):
        log.debug(msg)

    def loginf(msg):
        log.info(msg)

    def logerr(msg):
        log.error(msg)

    # log_traceback() generates the same output but the signature and code is
    # different between v3 and v4. We only need log_traceback at the log.error
    # level so define a suitable wrapper function.
    def log_traceback_critical(prefix=''):
        log_traceback(log.critical, prefix=prefix)

    def log_traceback_error(prefix=''):
        log_traceback(log.error, prefix=prefix)

    def log_traceback_debug(prefix=''):
        log_traceback(log.debug, prefix=prefix)

except ImportError:
    # WeeWX legacy (v3) logging via syslog
    import syslog
    from weeutil.weeutil import log_traceback

    def logmsg(level, msg):
        syslog.syslog(level, 'gw1000: %s' % msg)

    def logdbg(msg):
        logmsg(syslog.LOG_DEBUG, msg)

    def loginf(msg):
        logmsg(syslog.LOG_INFO, msg)

    def logerr(msg):
        logmsg(syslog.LOG_ERR, msg)

    # log_traceback() generates the same output but the signature and code is
    # different between v3 and v4. We only need log_traceback at the log.error
    # level so define a suitable wrapper function.
    def log_traceback_critical(prefix=''):
        log_traceback(prefix=prefix, loglevel=syslog.LOG_CRIT)

    def log_traceback_error(prefix=''):
        log_traceback(prefix=prefix, loglevel=syslog.LOG_ERR)

    def log_traceback_debug(prefix=''):
        log_traceback(prefix=prefix, loglevel=syslog.LOG_DEBUG)

DRIVER_NAME = 'GW1000'
DRIVER_VERSION = '0.1.0b6'

# various defaults used throughout
# default port used by GW1000
default_port = 45000
# default network broadcast address - the address that network broadcasts are
# sent to
default_broadcast_address = '255.255.255.255'
# default network broadcast port - the port that network broadcasts are sent to
default_broadcast_port = 46000
# default socket timeout
default_socket_timeout = 2
# When run as a service the default age in seconds after which GW1000 API data
# is considered stale and will not be used to augment loop packets
default_max_age = 60
# default GW1000 poll interval
default_poll_interval = 60


# ============================================================================
#                          GW1000 API error classes
# ============================================================================


class InvalidApiResponse(Exception):
    """Exception raised when an API call response is invalid."""


class InvalidChecksum(Exception):
    """Exception raised when an API call response contains an invalid
    checksum."""


class UnknownCommand(Exception):
    """Exception raised when an unknown API command is used."""


# ============================================================================
#                               class Gw1000
# ============================================================================


class Gw1000(object):
    """Base class for interacting with a GW1000.

    There are a number of common properties and methods (eg IP address,
    field map, rain calculation etc) when dealing with a GW1000 either as a
    driver or service. This class captures those common features.
    """

    # Default field map to map GW1000 sensor data to WeeWX fields. WeeWX field
    # names are used where there is a direct correlation to the WeeWX extended
    # schema otherwise fields are passed passed through as is.
    # Format is:
    #   WeeWX field name: GW1000 field name
    default_field_map = {
        'inTemp': 'intemp',
        'outTemp': 'outtemp',
        'dewpoint': 'dewpoint',
        'windchill': 'windchill',
        'heatindex': 'heatindex',
        'inHumidity': 'inhumid',
        'outHumidity': 'outhumid',
        'pressure': 'absbarometer',
        'barometer': 'relbarometer',
        'windDir': 'winddir',
        'windSpeed': 'windspeed',
        'windGust': 'gustspeed',
        'rain': 'rain',
        'rainevent': 'rainevent',
        'rainRate': 'rainrate',
        'rainhour': 'rainhour',
        'rainday': 'rainday',
        'rainweek': 'rainweek',
        'rainmonth': 'rainmonth',
        'rainyear': 'rainyear',
        'raintotals': 'raintotals',
        'luminosity': 'light',
        'uvRadiation': 'uv',
        'UV': 'uvi',
        'dateTime': 'datetime',
        'daymaxwind': 'daymaxwind',
        'extraTemp1': 'temp1',
        'extraTemp2': 'temp2',
        'extraTemp3': 'temp3',
        'extraTemp4': 'temp4',
        'extraTemp5': 'temp5',
        'extraTemp6': 'temp6',
        'extraTemp7': 'temp7',
        'extraTemp8': 'temp8',
        'extraHumid1': 'humid1',
        'extraHumid2': 'humid2',
        'extraHumid3': 'humid3',
        'extraHumid4': 'humid4',
        'extraHumid5': 'humid5',
        'extraHumid6': 'humid6',
        'extraHumid7': 'humid7',
        'extraHumid8': 'humid8',
        'pm2_5': 'pm251',
        'pm2_52': 'pm252',
        'pm2_53': 'pm253',
        'pm2_54': 'pm254',
        'soilTemp1': 'soiltemp1',
        'soilMoist1': 'soilmoist1',
        'soilTemp2': 'soiltemp2',
        'soilMoist2': 'soilmoist2',
        'soilTemp3': 'soiltemp3',
        'soilMoist3': 'soilmoist3',
        'soilTemp4': 'soiltemp4',
        'soilMoist4': 'soilmoist4',
        'soilTemp5': 'soiltemp5',
        'soilMoist5': 'soilmoist5',
        'soilTemp6': 'soiltemp6',
        'soilMoist6': 'soilmoist6',
        'soilTemp7': 'soiltemp7',
        'soilMoist7': 'soilmoist7',
        'soilTemp8': 'soiltemp8',
        'soilMoist8': 'soilmoist8',
        'soilTemp9': 'soiltemp9',
        'soilMoist9': 'soilmoist9',
        'soilTemp10': 'soiltemp10',
        'soilMoist10': 'soilmoist10',
        'soilTemp11': 'soiltemp11',
        'soilMoist11': 'soilmoist11',
        'soilTemp12': 'soiltemp12',
        'soilMoist12': 'soilmoist12',
        'soilTemp13': 'soiltemp13',
        'soilMoist13': 'soilmoist13',
        'soilTemp14': 'soiltemp14',
        'soilMoist14': 'soilmoist14',
        'soilTemp15': 'soiltemp15',
        'soilMoist15': 'soilmoist15',
        'soilTemp16': 'soiltemp16',
        'soilMoist16': 'soilmoist16',
        '24havpm251': '24havpm251',
        '24havpm252': '24havpm252',
        '24havpm253': '24havpm253',
        '24havpm254': '24havpm254',
        'leak1': 'leak1',
        'leak2': 'leak2',
        'leak3': 'leak3',
        'leak4': 'leak4',
        'lightning_distance': 'lightningdist',
        'lightning_last_det_time': 'lightningdettime',
        'lightning_strike_count': 'lightning_strike_count'
    }

    def __init__(self, **gw1000_config):
        """Initialise a GW1000 object."""

        # construct the field map, first obtain the field map from our config
        field_map = gw1000_config.get('field_map')
        # obtain any field map extensions from our config
        extensions = gw1000_config.get('field_map_extensions', {})
        # if we have no field map then use the default
        if field_map is None:
            field_map = dict(Gw1000.default_field_map)
        # If a user wishes to rename a field from the default map they can
        # include an entry in field_map_extensions but that leaves the
        # original field map as well. This can be removed if the user adds
        # a an 'empty' entry in field_map_extensions for the now redundant
        # field from the default field map eg:
        # [[field_map_extensions]]
        #    dayRain = rainday
        #    rainday =
        # The first entry re-maps rainday to dayRain, the second entry
        # removes the map rainday to rainday in the default field map.
        # Do we have any field map extensions
        if len(extensions) > 0:
            # yes, make a copy of our field map extensions as we will need
            # to pop off any 'empty' entries
            field_map_extensions = dict(extensions)
            # iterate over the keys and values in the field map extensions
            for w,g in six.iteritems(extensions):
                # if we find an empty entry
                if g == '':
                    # pop off the entry from the field map
                    dummy = field_map.pop(w, None)
                    # and pop off the spent entry in the field map
                    # extensions
                    dummy = field_map_extensions.pop(w, None)
            # update our field map with any field map extensions
            field_map.update(field_map_extensions)
        # we now have our final field map
        self.field_map = field_map
        # network broadcast address and port
        self.broadcast_address = str.encode(gw1000_config.get('broadcast_address',
                                                              default_broadcast_address))
        self.broadcast_port = gw1000_config.get('broadcast_port',
                                                default_broadcast_port)
        self.socket_timeout = gw1000_config.get('socket_timeout',
                                                default_socket_timeout)
        # obtain the GW1000 ip address
        _ip_address = gw1000_config.get('ip_address')
        # if the user has specified some variation of 'auto' then we are to
        # automatically detect the GW1000 IP address, to do that we set the
        # ip_address property to None
        if _ip_address is not None:
            # do we have a variation of 'auto'
            if _ip_address.lower() == 'auto':
                # we need to autodetect ip address so set to None
                _ip_address = None
            else:
                # if the ip address is specified we need to encode it
                _ip_address = _ip_address.encode()
        # set the ip address property
        self.ip_address = _ip_address
        # obtain the GW1000 port from the config dict
        # for port number we have a default value we can use, so if port is not
        # specified use the default
        _port = gw1000_config.get('port', default_port)
        # if a port number was specified it needs to be an integer not a string
        # so try to do the conversion
        try:
            _port = int(_port)
        except TypeError:
            # most likely port somehow ended up being None, in any case force
            # autodetection by setting port to None
            _port = None
        except ValueError:
            # We couldn't convert the port number to an integer. Maybe it was
            # because it was 'auto' (or some variation) or perhaps it was
            # invalid. Either way we need to set port to None to force
            # autodetection. If there was an invalid port specified then log it.
            if _port.lower() != 'auto':
                loginf("Invalid GW1000 port '%s' specified, port will be autodetected" % (_port,))
            _port = None
        # set the port property
        self.port = _port
        # how many times to poll the API before giving up, default is 3
        self.max_tries = int(gw1000_config.get('max_tries', 3))
        # wait time in seconds between retries, default is 10 seconds
        self.retry_wait = int(gw1000_config.get('retry_wait', 10))
        # how often (in seconds) we should poll the API, default is 60 seconds
        self.poll_interval = int(gw1000_config.get('poll_interval', 60))
        # age (in seconds) before API data is considered too old to use,
        # default is 60 seconds
        self.max_age = int(gw1000_config.get('max_age', default_max_age))
        # Is a WH32 in use. WH32 TH sensor can override/provide outdoor TH data
        # to the GW1000. In tems of TH data the process is transparent and we
        # do not need to know if a WH32 or other sensor is providing outdoor TH
        # data but in terms of battery state we need to know so the battery
        # state data can be reported against the correct sensor.
        use_th32 = weeutil.weeutil.tobool(gw1000_config.get('th32', False))
        # create an Gw1000Collector object to interact with the GW1000 API
        self.collector = Gw1000Collector(ip_address=self.ip_address,
                                         port=self.port,
                                         broadcast_address=self.broadcast_address,
                                         broadcast_port=self.broadcast_port,
                                         socket_timeout=self.socket_timeout,
                                         poll_interval=self.poll_interval,
                                         max_tries=self.max_tries,
                                         retry_wait=self.retry_wait,
                                         use_th32=use_th32)
        # initialise last lightning count and last rain properties
        self.last_lightning = None
        self.last_rain = None
        self.rain_mapping_confirmed = False
        self.rain_total_field = None
        # finally log any config that is not being pushed any further down
        # sensor map to be used
        # Dict output will be in unsorted key order. It is easier to read if
        # sorted alphanumerically but we have keys such as xxxxx16 that do not
        # sort well. Use a custom natural sort of the keys in a manually
        # produced formatted dict representation.
        sorted_dict_fields = ["'%s': '%s'" % (k, self.field_map[k]) for k in natural_sort_dict(self.field_map)]
        sorted_dict_str = "{%s}" % ", ".join(sorted_dict_fields)
        loginf('field map is %s' % sorted_dict_str)

    def map_data(self, data):
        """Map parsed GW1000 data to a WeeWX loop packet.

        Maps parsed GW1000 data to WeeWX loop packet fields using the field map.
        Result includes usUnits field set to METRICWX.

        data: Dict of parsed GW1000 API data
        """

        # parsed GW1000 API data uses the METRICWX unit system
        _result = {'usUnits': weewx.METRICWX}
        # iterate over each of the key, value pairs in the field map
        for weewx_field, data_field in six.iteritems(self.field_map):
            # if the field to be mapped exists in the data obtain it's
            # value and map it to the packet
            if data_field in data:
                _result[weewx_field] = data.get(data_field)
        return _result

    def get_cumulative_rain_field(self, parsed_data):
        """Determine the cumulative rain field used to derive field 'rain'.

        Ecowitt rain gauges/GW1000 emit various rain totals but WeeWX needs a
        per period value for field rain. Try the 'big' (4 byte) counters
        starting at the longest period and working our way down. This should
        only need be done once.
        """

        if 'raintotals' in parsed_data:
            self.rain_total_field = 'raintotals'
            self.rain_mapping_confirmed = True
        elif 'rainyear' in parsed_data:
            self.rain_total_field = 'rainyear'
            self.rain_mapping_confirmed = True
        elif 'rainmonth' in parsed_data:
            self.rain_total_field = 'rainmonth'
            self.rain_mapping_confirmed = True
        else:
            self.rain_total_field = None
        if self.rain_mapping_confirmed:
            loginf("using '%s' for rain total" % self.rain_total_field)

    def calculate_rain(self, parsed_data):
        """Calculate total rainfall for a period.

        'rain' is calculated as the change in a user designated cumulative rain
        field between successive periods. 'rain' is only calculated if the
        field to be used has been selected and the designated field exists.
        """

        if self.rain_mapping_confirmed and self.rain_total_field in parsed_data:
            new_total = parsed_data[self.rain_total_field]
            parsed_data['rain'] = self.delta_rain(new_total, self.last_rain)
            self.last_rain = new_total

    def calculate_lightning_count(self, parsed_data):
        """Calculate total lightning strike count for a period.

        'lightning_strike_count' is calculated as the change in field
        'lighningcount' between successive periods. 'lightning_strike_count' is
        only calculated if 'lightningcount' exists.
        """

        if 'lightningcount' in parsed_data:
            new_total = parsed_data['lightningcount']
            parsed_data['lightning_strike_count'] = self.delta_lightning(new_total,
                                                                         self.last_lightning)
            self.last_lightning = new_total

    @staticmethod
    def delta_rain(rain, last_rain):
        """Calculate rainfall from successive cumulative values.

        Rainfall is calculated as the difference between two cumulative values.
        If either value is None the value None is returned. If the previous
        value is greater than the latest value a counter wrap around is assumed
        and the latest value is returned.
        """

        # do we have a last rain value
        if last_rain is None:
            # no, log it and return None
            loginf("skipping rain measurement of %s: no last rain" % rain)
            return None
        # do we have a non-None current rain value
        if rain is None:
            # no, log it and return None
            loginf("skipping rain measurement: no current rain")
            return None
        # is the last rain value greater than the current rain value
        if rain < last_rain:
            # it is, assume a counter wrap around/reset, log it and return the
            # latest rain value
            loginf("rain counter wraparound detected: new=%s last=%s" % (rain, last_rain))
            return rain
        # otherwise return the difference between the counts
        return rain - last_rain

    @staticmethod
    def delta_lightning(count, last_count):
        """Calculate lightning strike count from successive cumulative values.

        Lightning strike count is calculated as the difference between two
        cumulative values. If either value is None the value None is returned.
        If the previous value is greater than the latest value a counter wrap
        around is assumed and the latest value is returned.
        """

        # do we have a last count
        if last_count is None:
            # no, log it and return None
            loginf("skipping lightning count of %s: no last count" % count)
            return None
        # do we have a non-None current count
        if count is None:
            # no, log it and return None
            loginf("skipping lightning count: no current count")
            return None
        # is the last count greater than the current count
        if count < last_count:
            # it is, assume a counter wrap around/reset, log it and return the
            # latest count
            loginf("lightning counter wraparound detected: new=%s last=%s" % (count, last_count))
            return count
        # otherwise return the difference between the counts
        return count - last_count


# ============================================================================
#                            GW1000 Service class
# ============================================================================


class Gw1000Service(weewx.engine.StdService, Gw1000):
    """GW1000 service class.

    A WeeWX service to augment loop packets with observational data obtained
    from the GW1000 API. Using the Gw1000Service is useful when data is
    required from more than one source, for example, WeeWX is using another
    driver and the Gw1000Driver cannot be used.

    Data is obtained from the GW1000 API. The data is parsed and mapped to
    WeeWX fields and if the GW1000 data is not stale the loop packets is
    augmented with the GW1000 mapped data.

    Class Gw1000Collector collects and parses data from the GW1000 API. The
    Gw1000Collector runs in a separate thread so as to not block the main WeeWX
    processing loop. The Gw1000Collector is turn uses child classes Station and
    Parser to interact directly with the GW1000 API and parse the API responses
    respectively.
    """

    def __init__(self, engine, config_dict):
        """Initialise a Gw1000Service object."""

        # extract the GW1000 service config dictionary
        gw1000_config_dict = config_dict.get('Gw1000Service', {})
        # initialize my superclasses
        super(Gw1000Service, self).__init__(engine, config_dict)
        super(weewx.engine.StdService, self).__init__(**gw1000_config_dict)

        # log our version number
        loginf('version is %s' % DRIVER_VERSION)
        # log the relevant settings/parameters we are using
        if self.ip_address is None and self.port is None:
            loginf("GW1000 IP address and port not specified, attempting to discover GW1000...")
        elif self.ip_address is None:
            loginf("GW1000 IP address not specified, attempting to discover GW1000...")
        elif self.port is None:
            loginf("GW1000 port not specified, attempting to discover GW1000...")
        loginf("GW1000 address is %s:%d" % (self.collector.station.ip_address.decode(),
                                            self.collector.station.port))
        loginf("poll interval is %d seconds" % self.poll_interval)
        logdbg('max tries is %d, retry wait time is %d seconds' % (self.max_tries,
                                                                   self.retry_wait))
        logdbg('broadcast address %s:%d, socket timeout is %d seconds' % (self.broadcast_address,
                                                                          self.broadcast_port,
                                                                          self.socket_timeout))
        loginf("max age of API data to be used is %d seconds" % self.max_age)
        # start the Gw1000Collector in its own thread
        self.collector.startup()
        # bind our self to the relevant weeWX events
        self.bind(weewx.NEW_LOOP_PACKET, self.new_loop_packet)

    def new_loop_packet(self, event):
        """Augment a loop packet with GW1000 data.

        When a new loop packet arrives check for any GW1000 data in the queue
        and if available and not stale map the data to WeeWX fields and use the
        mapped data to augment the loop packet.
        """

        # Check the queue to get the latest GW1000 sensor data. Wrap in a try
        # to catch any instances where the queue is empty but also be prepared
        # to pop off any old records to get the most recent.
        try:
            # get any day from the collector queue, but don't dwell very long
            parsed_data = self.collector.queue.get(True, 0.5)
        except six.moves.queue.Empty:
            # there was nothing in the queue so continue
            pass
        else:
            # we got something out of the queue but only process it if it was
            # not None
            if parsed_data is not None:
                # if not already determined determine which cumulative rain
                # field will be used to determine the per period rain field
                if not self.rain_mapping_confirmed:
                    self.get_cumulative_rain_field(parsed_data)
                # get the rainfall this period from total
                self.calculate_rain(parsed_data)
                # get the lightning strike count this period from total
                self.calculate_lightning_count(parsed_data)
                # map the raw data to WeeWX fields
                mapped_data = self.map_data(parsed_data)
                # and finally augment the loop packet with the mapped data
                self.augment_packet(event.packet, mapped_data)
                # log the augmented packet but only if debug>=2
                if weewx.debug >= 2:
                    logdbg('Augmented packet: %s' % event.packet)

    def augment_packet(self, packet, data):
        """Augment a loop packet with data from another packet.

        If the data to be used to augment the loop packet is not stale then
        augment the loop packet with the data concerned. The data to be
        used to augment the lop packet is assumed to contain a field 'usUnits'
        designating the unit system of the data to be used for augmentation.
        The data to be used for augmentation is converted to the same unit
        system as used in the loop packet before augmentation occurs. Only
        fields that exist in the data used for augmentation but not in the loop
        packet are added to the loop packet.

        packet: dict containing the loop packet
        data: dict containing the data to be used to augment the loop packet
        """

        if 'dateTime' in data and data['dateTime'] > packet['dateTime'] - self.max_age:
            # get a converter
            converter = weewx.units.StdUnitConverters[packet['usUnits']]
            # convert the mapped data to the same unit system as the packet to
            # be augmented
            converted_data = converter.convertDict(data)
            # now we can freely augment the packet with any of our mapped obs
            for field, data in six.iteritems(converted_data):
                if field not in packet:
                    packet[field] = data

    def shutDown(self):
        """Shut down the service."""

        # the collector will likely be running in a thread so call its
        # shutdown() method so that any thread shut down/tidy up can occur
        self.collector.shutdown()


# ============================================================================
#                 GW1000 Loader/Configurator/Editor methods
# ============================================================================


def loader(config_dict, engine):

    return Gw1000Driver(**config_dict[DRIVER_NAME])


def configurator_loader(config_dict):  # @UnusedVariable

    pass
    # return Gw1000Configurator()


def confeditor_loader():

    return Gw1000ConfEditor()

# ============================================================================
#                          class Gw1000ConfEditor
# ============================================================================


class Gw1000ConfEditor(weewx.drivers.AbstractConfEditor):

    @property
    def default_stanza(self):
        return """
    [GW1000]
        # This section is for the GW1000 API driver.

        # The driver to use:
        driver = user.gw1000
        
        # How often to poll the GW1000 API:
        poll_interval = 60
    """

    def prompt_for_settings(self):

        print("Specify GW1000 IP address, for example: 192.168.1.100")
        print("Set to 'auto' to autodiscover GW1000 IP address")
        ip_address = self._prompt('IP address')
        print("Specify GW1000 network port, for example: 45000")
        port = self._prompt('port', default_port)
        print("Specify how often to poll the GW1000 API in seconds")
        poll_interval = self._prompt('Poll interval', default_poll_interval)
        return {'ip_address': ip_address,
                'port': port,
                'poll_interval': poll_interval
                }

    @staticmethod
    def modify_config(config_dict):

        print("""Setting record_generation to software.""")
        config_dict['StdArchive']['record_generation'] = 'software'
        print("""Setting lightning count extractor to sum.""")
        if 'Accumulator' in config_dict:
            config_dict['Accumulator']['lightning_strike_count'] = {'extractor': 'sum'}
        else:
            config_dict['Accumulator'] = {'lightning_strike_count': {'extractor': 'sum'}}


# ============================================================================
#                            GW1000 Driver class
# ============================================================================


class Gw1000Driver(weewx.drivers.AbstractDevice, Gw1000):
    """GW1000 driver class.

    A WeeWX driver to emit loop packets based on observational data obtained
    from the GW1000 API. The Gw1000Driver should be used when there is no other
    data source or other sources data can be ingested via one or more WeeWX
    services.

    Data is obtained from the GW1000 API. The data is parsed and mapped to
    WeeWX fields and emitted as a WeeWX loop packet.

    Class Gw1000Collector collects and parses data from the GW1000 API. The
    Gw1000Collector runs in a separate thread so as to not block the main WeeWX
    processing loop. The Gw1000Collector is turn uses child classes Station and
    Parser to interact directly with the GW1000 API and parse the API responses
    respectively."""

    def __init__(self, **stn_dict):
        """Initialise a GW1000 driver object."""

        # initialize my superclasses
        super(Gw1000Driver, self).__init__(**stn_dict)

        # log our version number
        loginf('driver version is %s' % DRIVER_VERSION)
        # log the relevant settings/parameters we are using
        if self.ip_address is None and self.port is None:
            loginf("GW1000 IP address and port not specified, attempting to discover GW1000...")
        elif self.ip_address is None:
            loginf("GW1000 IP address not specified, attempting to discover GW1000...")
        elif self.port is None:
            loginf("GW1000 port not specified, attempting to discover GW1000...")
        loginf("GW1000 address is %s:%d" % (self.collector.station.ip_address.decode(),
                                            self.collector.station.port))
        loginf("poll interval is %d seconds" % self.poll_interval)
        logdbg('max tries is %d, retry wait time is %d seconds' % (self.max_tries,
                                                                   self.retry_wait))
        logdbg('broadcast address %s:%d, socket timeout is %d seconds' % (self.broadcast_address,
                                                                          self.broadcast_port,
                                                                          self.socket_timeout))
        # start the Gw1000Collector in its own thread
        self.collector.startup()

    def genLoopPackets(self):
        """Generator function that returns loop packets.

        Run a continuous loop checking the Gw1000Collector queue for data. When
        data arrives map the raw data to a WeeWX loop packet and yield the
        packet.
        """

        # generate loop packets forever
        while True:
            # wrap in a try to catch any instances where the queue is empty
            try:
                # get any day from the collector queue
                parsed_data = self.collector.queue.get(True, 10)
            except six.moves.queue.Empty:
                # there was nothing in the queue so continue
                pass
            else:
                # create a loop packet and initialise with dateTime and usUnits
                packet = {'dateTime': int(time.time() + 0.5)}
                # if not already determined determine which cumulative rain
                # field will be used to determine the per period rain field
                if not self.rain_mapping_confirmed:
                    self.get_cumulative_rain_field(parsed_data)
                # get the rainfall this period from total
                self.calculate_rain(parsed_data)
                # get the lightning strike count this period from total
                self.calculate_lightning_count(parsed_data)
                # map the raw data to WeeWX loop packet fields
                mapped_data = self.map_data(parsed_data)
                # add the mapped data to the empty packet
                packet.update(mapped_data)
                # log the packet but only if debug>=2
                if weewx.debug >= 2:
                    logdbg('Packet: %s' % packet)
                # yield the loop packet
                yield packet

    @property
    def hardware_name(self):
        """Return the hardware name."""

        return DRIVER_NAME

    @property
    def mac_address(self):
        """Return the GW1000 MAC address."""

        return self.collector.mac_address

    @property
    def firmware_version(self):
        """Return the GW1000 firmware version string."""

        return self.collector.firmware_version

    @property
    def sensor_id_data(self):
        """Return the GW1000 sensor identification data."""

        return self.collector.sensor_id_data

    def closePort(self):
        """Close down the driver port."""

        # in this case there is no port to close, just the collector thread
        self.collector.shutdown()


# ============================================================================
#                              class Collector
# ============================================================================


class Collector(object):
    """Base class for a client that polls an API."""

    # a queue object for passing data back to the driver
    queue = six.moves.queue.Queue()

    def __init__(self):
        pass

    def startup(self):
        pass

    def shutdown(self):
        pass


# ============================================================================
#                              class Gw1000Collector
# ============================================================================


class Gw1000Collector(Collector):
    """Class to poll the GW1000 API then decode and return data to the driver."""

    # map of sensor ids to short and long names
    sensor_ids = {
        b'\x00': {'name': 'wh65', 'long_name': 'WH65'},
        b'\x01': {'name': 'ws68', 'long_name': 'WS68'},
        b'\x02': {'name': 'ws80', 'long_name': 'WS80'},
        b'\x03': {'name': 'wh40', 'long_name': 'WH40'},
        b'\x04': {'name': 'wh25', 'long_name': 'WH25'},
        b'\x05': {'name': 'wh26', 'long_name': 'WH26'},
        b'\x06': {'name': 'wh31_ch1', 'long_name': 'WH31 ch1'},
        b'\x07': {'name': 'wh31_ch2', 'long_name': 'WH31 ch2'},
        b'\x08': {'name': 'wh31_ch3', 'long_name': 'WH31 ch3'},
        b'\x09': {'name': 'wh31_ch4', 'long_name': 'WH31 ch4'},
        b'\x0a': {'name': 'wh31_ch5', 'long_name': 'WH31 ch5'},
        b'\x0b': {'name': 'wh31_ch6', 'long_name': 'WH31 ch6'},
        b'\x0c': {'name': 'wh31_ch7', 'long_name': 'WH31 ch7'},
        b'\x0d': {'name': 'wh31_ch8', 'long_name': 'WH31 ch8'},
        b'\x0e': {'name': 'wh51_ch1', 'long_name': 'WH51 ch1'},
        b'\x0f': {'name': 'wh51_ch2', 'long_name': 'WH51 ch2'},
        b'\x10': {'name': 'wh51_ch3', 'long_name': 'WH51 ch3'},
        b'\x11': {'name': 'wh51_ch4', 'long_name': 'WH51 ch4'},
        b'\x12': {'name': 'wh51_ch5', 'long_name': 'WH51 ch5'},
        b'\x13': {'name': 'wh51_ch6', 'long_name': 'WH51 ch6'},
        b'\x14': {'name': 'wh51_ch7', 'long_name': 'WH51 ch7'},
        b'\x15': {'name': 'wh51_ch8', 'long_name': 'WH51 ch8'},
        b'\x16': {'name': 'wh41_ch1', 'long_name': 'WH41 ch1'},
        b'\x17': {'name': 'wh41_ch2', 'long_name': 'WH41 ch2'},
        b'\x18': {'name': 'wh41_ch3', 'long_name': 'WH41 ch3'},
        b'\x19': {'name': 'wh41_ch4', 'long_name': 'WH41 ch4'},
        b'\x1a': {'name': 'wh57', 'long_name': 'WH57'},
        b'\x1b': {'name': 'wh55_ch1', 'long_name': 'WH55 ch1'},
        b'\x1c': {'name': 'wh55_ch2', 'long_name': 'WH55 ch2'},
        b'\x1d': {'name': 'wh55_ch3', 'long_name': 'WH55 ch3'},
        b'\x1e': {'name': 'wh55_ch4', 'long_name': 'WH55 ch4'},
        b'\x1f': {'name': 'wh34_ch1', 'long_name': 'WH34 ch1'},
        b'\x20': {'name': 'wh34_ch2', 'long_name': 'WH34 ch2'},
        b'\x21': {'name': 'wh34_ch3', 'long_name': 'WH34 ch3'},
        b'\x22': {'name': 'wh34_ch4', 'long_name': 'WH34 ch4'},
        b'\x23': {'name': 'wh34_ch5', 'long_name': 'WH34 ch5'},
        b'\x24': {'name': 'wh34_ch6', 'long_name': 'WH34 ch6'},
        b'\x25': {'name': 'wh34_ch7', 'long_name': 'WH34 ch7'},
        b'\x26': {'name': 'wh34_ch8', 'long_name': 'WH34 ch8'}
    }

    def __init__(self, ip_address=None, port=None,
                 broadcast_address=None, broadcast_port=None,
                 socket_timeout=None, poll_interval=60,
                 max_tries=3, retry_wait=10, use_th32=False):
        """Initialise our class."""

        # initialize my base class:
        super(Gw1000Collector, self).__init__()

        # interval between polls of the API, default is 60 seconds
        self.poll_interval = poll_interval
        # how many times to poll the API before giving up, default is 3
        self.max_tries = max_tries
        # period in seconds to wait before polling again, default is 10 seconds
        self.retry_wait = retry_wait
        # arewe using a th32 sensor
        self.use_th32 = use_th32
        # get a station object to do the handle the interaction with the
        # GW1000 API
        self.station = Gw1000Collector.Station(ip_address=ip_address,
                                               port=port,
                                               broadcast_address=broadcast_address,
                                               broadcast_port=broadcast_port,
                                               socket_timeout=socket_timeout,
                                               max_tries=max_tries,
                                               retry_wait=retry_wait)
        # Do we have a WH24 attached? First obtain our system parameters.
        _sys_params = self.station.get_system_params()
        # WH24 is indicated by the 6th byte being 0
        is_wh24 = six.indexbytes(_sys_params, 5) == 0
        # Tell our sensor id decoding whether we have a WH24 or a WH65. By
        # default we are coded to use a WH65. Is there a WH24 connected?
        if is_wh24:
            # set the WH24 sensor id decode dict entry
            self.sensor_ids[ b'\x00']['name'] = 'wh24'
            self.sensor_ids[b'\x00']['long_name'] = 'WH24'
        # get a parser object to parse any data from the station
        self.parser = Gw1000Collector.Parser(is_wh24)
        self._thread = None
        self._collect_data = False

    def collect_sensor_data(self):
        """Collect sensor data by polling the API.

        Loop forever waking periodically to see if it is time to quit.
        """

        # initialise ts of last time API was polled
        last_poll = 0
        # collect data continuously while we are told to collect data
        while self._collect_data:
            now = time.time()
            # is it time to poll?
            if now - last_poll > self.poll_interval:
                # it is time to poll
                filtered_data = self.get_live_sensor_data()
                # did we get any data
                if filtered_data is not None:
                    # put the data in the queue
                    self.queue.put(filtered_data)
                # reset the last poll ts
                last_poll = now
                # debug log when we will next poll the API
                logdbg('Next update in %s seconds' % self.poll_interval)
            # sleep for a second and then see if its time to poll again
            time.sleep(1)

    def get_live_sensor_data(self):
        """Get sensor data.

        Obtain live sensor data from the GW1000 API. Parse the API response.
        The parsed battery data is then further processed to filter out battery
        state data for non-existent sensors. The filtered data is returned as a
        dict. If no data was obtained from the API the value None is returned.
        """

        # obtain the raw data via the GW1000 API
        raw_data = self.station.get_livedata()
        # get a timestamp to use in case our data does not come with one
        _timestamp = int(time.time())
        if raw_data is not None:
            # parse the raw data
            parsed_data = self.parser.parse(raw_data, _timestamp)
            # log the parsed data but only if debug>=3
            if weewx.debug >= 3:
                logdbg("Parsed data: %s" % parsed_data)
            filtered_data = self.filter_battery_data(parsed_data)
            # log the filtered parsed data but only if debug>=3
            if weewx.debug >= 3:
                logdbg("Filtered parsed data: %s" % filtered_data)
            return filtered_data
        else:
            # we did not get any data so log it and continue
            logerr("Failed to get sensor data")
        return None

    def filter_battery_data(self, parsed_data):
        """Filter out battery data for unused sensors.

        The battery status data returned by the GW1000 API does not allow the
        discrimination of all used/unused sensors (it does for some but not for
        others). Some further processing of the battery status data is required
        to ensure that battery status is only provided for sensors that
        actually exist.
        """

        # tuple of values for sensors that are not registered with the GW1000
        not_registered = ('fffffffe', 'ffffffff')
        # obtain details of the sensors from the GW1000 API
        sensor_list = self.sensor_id_data
        # determine which sensors are registered, these are the sensors for
        # which we desire battery state information
        registered_sensors = [s['address'] for s in sensor_list if s['id'] not in not_registered]
        # obtain a list of registered sensor names
        reg_sensor_names = [Gw1000Collector.sensor_ids[a]['name'] for a in registered_sensors]
        # obtain a copy of our parsed data as we are going to alter it
        filtered = dict(parsed_data)
        # iterate over the parsed data
        for key, data in six.iteritems(parsed_data):
            # obtain the sensor name from any any battery fields
            stripped = key[:-5] if key.endswith('_batt') else key
            # if field is a battery state field, and the field pertains to an
            # unregistered sensor, remove the field from the parsed data
            if '_batt' in key and stripped not in reg_sensor_names:
                del filtered[key]
        # return our parsed data with battery state information fo unregistered
        # sensors removed
        return filtered

    @property
    def rain_data(self):
        """Obtain GW1000 rain data."""

        # obtain the rain data data via the API
        response = self.station.get_raindata()
        # determine the size of the rain data
        raw_data_size = six.indexbytes(response, 3)
        # extract the actual data
        data = response[4:4 + raw_data_size - 3]
        # initialise a dict to hold our final data
        data_dict = dict()
        data_dict['rain_rate'] = self.parser.decode_big_rain(data[0:4])
        data_dict['rain_day'] = self.parser.decode_big_rain(data[4:8])
        data_dict['rain_week'] = self.parser.decode_big_rain(data[8:12])
        data_dict['rain_month'] = self.parser.decode_big_rain(data[12:16])
        data_dict['rain_year'] = self.parser.decode_big_rain(data[16:20])
        return data_dict

    @property
    def system_parameters(self):
        """Obtain GW1000 system parameters."""

        # obtain the system parameters data via the API
        response = self.station.get_system_params()
        # determine the size of the system parameters data
        raw_data_size = six.indexbytes(response, 3)
        # extract the actual system parameters data
        data = response[4:4 + raw_data_size - 3]
        # initialise a dict to hold our final data
        data_dict = dict()
        data_dict['frequency'] = six.indexbytes(data, 0)
        data_dict['sensor_type'] = six.indexbytes(data, 1)
        data_dict['utc'] = self.parser.decode_utc(data[2:6])
        data_dict['timezone'] = six.indexbytes(data, 6)
        return data_dict

    @property
    def mac_address(self):
        """Obtain the MAC address of the GW1000."""

        station_mac_b = self.station.get_mac_address()
        return self.bytes_to_hex(station_mac_b[4:10], separator=":")

    @property
    def firmware_version(self):
        """Obtain the GW1000 firmware version string."""

        firmware_b = self.station.get_firmware_version()
        firmware_format = "B" * len(firmware_b)
        firmware_t = struct.unpack(firmware_format, firmware_b)
        return "".join([chr(x) for x in firmware_t[5:18]])

    @property
    def sensor_id_data(self):
        """Get sensor id data.

        """

        # obtain the sensor id data via the API
        response = self.station.get_sensor_id()
        # determine the size of the sensor id data
        raw_data_size = six.indexbytes(response, 3)
        # extract the actual sensor id data
        data = response[4:4 + raw_data_size - 3]
        # initialise a counter
        index = 0
        # initialise a list to hold our final data
        sensor_id_list = []
        # iterate over
        while index < len(data):
            sensor_id = self.bytes_to_hex(data[index + 1: index + 5],
                                          separator='',
                                          caps=False)
            sensor_id_list.append({'address': data[index:index + 1],
                                   'id': sensor_id,
                                   'signal': six.indexbytes(data, index + 5),
                                   'battery': six.indexbytes(data, index + 6)
                                   })
            index += 7
        return sensor_id_list

    def startup(self):
        """Start a thread that collects data from the GW1000 API."""

        self._thread = Gw1000Collector.CollectorThread(self)
        self._collect_data = True
        self._thread.setDaemon(True)
        self._thread.setName('Gw1000CollectorThread')
        self._thread.start()

    def shutdown(self):
        """Shut down the thread that collects data from the GW1000 API.

        Tell the thread to stop, then wait for it to finish.
        """

        # we only need do something if a thread exists
        if self._thread:
            # tell the thread to stop collecting data
            self._collect_data = False
            # terminate the thread
            self._thread.join(10.0)
            # log the outcome
            if self._thread.isAlive():
                logerr("Unable to shut down Gw1000Collector thread")
            else:
                logdbg("Gw1000Collector thread has been terminated")
        self._thread = None

    @staticmethod
    def bytes_to_hex(iterable, separator=' ', caps=True):
        """Produce a hex string representation of a sequence of bytes."""

        # assume 'iterable' can be iterated by iterbytes and the individual
        # elements can be formatted with {:02X}
        format_str = "{:02X}" if caps else "{:02x}"
        try:
            return separator.join(format_str.format(c) for c in six.iterbytes(iterable))
        except (TypeError, ValueError):
            # ValueError - cannot format c as {:02X}
            # TypeError - 'iterable' is not iterable
            # either way we can't represent as a string of hex bytes
            return "cannot represent '%s' as hexadecimal bytes" % (iterable,)

    class CollectorThread(threading.Thread):
        """Class used to collect data via the GW1000 API in a thread."""

        def __init__(self, client):
            # initialise our parent
            threading.Thread.__init__(self)
            # keep reference to the client we are supporting
            self.client = client
            self.name = 'gw1000-client'

        def run(self):
            # rather than letting the thread silently fail if an exception
            # occurs within the thread, wrap in a try..except so the exception
            # can be caught and available exception information displayed
            try:
                # kick the collection off
                self.client.collect_sensor_data()
            except:
                # we have an exception so log what we can
                log_traceback_critical('    ****  ')

    class Station(object):
        """Class to interact directly with the GW1000 API.

        A Station object knows how to:
        1.  discover a GW1000 via UDP broadcast
        2.  send a command to the GW1000 API
        3.  receive a response from the GW1000 API
        4.  verify the response as valid

        A Station object needs an ip address and port as well as a network
        broadcast address and port.
        """

        # GW1000 API commands
        commands = {
            'CMD_WRITE_SSID': b'\x11',
            'CMD_BROADCAST': b'\x12',
            'CMD_READ_ECOWITT': b'\x1E',
            'CMD_WRITE_ECOWITT': b'\x1F',
            'CMD_READ_WUNDERGROUND': b'\x20',
            'CMD_WRITE_WUNDERGROUND': b'\x21',
            'CMD_READ_WOW': b'\x22',
            'CMD_WRITE_WOW': b'\x23',
            'CMD_READ_WEATHERCLOUD': b'\x24',
            'CMD_WRITE_WEATHERCLOUD': b'\x25',
            'CMD_READ_STATION_MAC': b'\x26',
            'CMD_READ_CUSTOMIZED': b'\x2A',
            'CMD_WRITE_CUSTOMIZED': b'\x2B',
            'CMD_WRITE_UPDATE': b'\x43',
            'CMD_READ_FIRMWARE_VERSION': b'\x50',
            'CMD_READ_USR_PATH': b'\x51',
            'CMD_WRITE_USR_PATH': b'\x52',
            'CMD_GW1000_LIVEDATA': b'\x27',
            'CMD_GET_SOILHUMIAD': b'\x28',
            'CMD_SET_SOILHUMIAD': b'\x29',
            'CMD_GET_MulCH_OFFSET': b'\x2C',
            'CMD_SET_MulCH_OFFSET': b'\x2D',
            'CMD_GET_PM25_OFFSET': b'\x2E',
            'CMD_SET_PM25_OFFSET': b'\x2F',
            'CMD_READ_SSSS': b'\x30',
            'CMD_WRITE_SSSS': b'\x31',
            'CMD_READ_RAINDATA': b'\x34',
            'CMD_WRITE_RAINDATA': b'\x35',
            'CMD_READ_GAIN': b'\x36',
            'CMD_WRITE_GAIN': b'\x37',
            'CMD_READ_CALIBRATION': b'\x38',
            'CMD_WRITE_CALIBRATION': b'\x39',
            'CMD_READ_SENSOR_ID': b'\x3A',
            'CMD_WRITE_SENSOR_ID': b'\x3B',
            'CMD_WRITE_SENSOR_ID_NEW': b'\x3C',
            'CMD_WRITE_REBOOT': b'\x40',
            'CMD_WRITE_RESET': b'\x41'
        }
        # header used in each API command and response packet
        header = b'\xff\xff'

        def __init__(self, ip_address=None, port=None,
                     broadcast_address=None, broadcast_port=None,
                     socket_timeout=None, max_tries=3, retry_wait=5):

            # network broadcast address
            self.broadcast_address = broadcast_address if broadcast_address is not None else default_broadcast_address
            # network broadcast port
            self.broadcast_port = broadcast_port if broadcast_port is not None else default_broadcast_port
            self.socket_timeout = socket_timeout if socket_timeout is not None else default_socket_timeout
            # if ip address or port was not specified (None) then attempt to
            # discover the GW1000 with a UDP broadcast
            if ip_address is None or port is None:
                for attempt in range(max_tries):
                    try:
                        # discover() returns a list of (ip address, port) tuples
                        ip_port_list = self.discover()
                    except socket.error as e:
                        logerr("Unable to detect GW1000 ip address and port: %s (%s)" % (e, type(e)))
                        log_traceback_critical("    ****  ")
                        # signal that we have a critical error
                        raise weewx.ViolatedPrecondition(e)
                    # did we find any GW1000
                    if len(ip_port_list) > 0:
                        # we have at least one, arbitrarily choose the first one
                        # found as the one to use
                        disc_ip = ip_port_list[0][0]
                        disc_port = ip_port_list[0][1]
                        # log the fact as well as what we found
                        gw1000_str = ', '.join([':'.join(['%s:%d' % b]) for b in ip_port_list])
                        if len(ip_port_list) == 1:
                            stem = "GW1000 was"
                        else:
                            stem = "Multiple GW1000 were"
                        loginf("%s found at %s" % (stem, gw1000_str))
                        ip_address = disc_ip.encode() if ip_address is None else ip_address.encode()
                        port = disc_port if port is None else port
                        break
                    else:
                        # did not discover any GW1000, log it and wait then try again
                        logdbg("Failed attempt %d to detect GW1000 ip address and port" % (attempt + 1,))
                        time.sleep(retry_wait)
                else:
                    # if we made it here we failed after max_tries attempts, log it and fail hard
                    logerr("Failed to detect GW1000 ip address and port after %d attempts" % (attempt + 1,))
                    # signal that we have a critical error
                    raise weewx.ViolatedPrecondition("GW1000 not found, you may need to specify "
                                                     "the GW1000 ip address and port in weewx.conf")
            self.ip_address = ip_address
            self.port = port
            self.max_tries = max_tries
            self.retry_wait = retry_wait

        def discover(self):
            """Discover any GW1000s on the local network.

            Send a UDP broadcast and check for replies. Decode each reply to
            obtain the IP address and port number of any GW1000s on the local
            network. Since there may be multiple GW1000s on the network
            package each IP address and port as a two way tuple and construct a
            list of unique IP address/port tuples. When complete return the
            list of IP address/port tuples found.
            """

            # now create a socket object so we can broadcast to the network
            # use IPv4 UDP
            socket_obj = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            # set socket datagram to broadcast
            socket_obj.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
            # set timeout
            socket_obj.settimeout(self.socket_timeout)
            # set TTL to 1 to so messages do not go past the local network
            # segment
            ttl = struct.pack('b', 1)
            socket_obj.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
            # Create packet to broadcast. Format is:
            #   Header, GW1000 Broadcast command, Size, Checksum
            size = len(b''.join([self.header, self.commands['CMD_BROADCAST']]))
            _header_data_size = b''.join([self.header, self.commands['CMD_BROADCAST'],
                                          struct.pack('B', size)])
            checksum = struct.pack('B',
                                   struct.unpack('B', self.commands['CMD_BROADCAST'])[0] + size)
            packet = b''.join([_header_data_size, checksum])
            if weewx.debug >= 3:
                logdbg("Sending broadcast packet '%s' to '%s:%d'" % (Gw1000Collector.bytes_to_hex(packet),
                                                                     self.broadcast_address,
                                                                     self.broadcast_port))
            # create a list for the results as multiple GW1000 may respond
            result_list = []
            try:
                # send the Broadcast command
                socket_obj.sendto(packet, (self.broadcast_address, self.broadcast_port))
                # obtain any responses
                while True:
                    try:
                        response = socket_obj.recv(1024)
                        # log the response if debug is high enough
                        if weewx.debug >= 3:
                            logdbg("Received broadcast response '%s'" % (Gw1000Collector.bytes_to_hex(response),))
                    except socket.timeout:
                        # if we timeout then we are done
                        break
                    except socket.error:
                        # raise any other socket error
                        raise
                    # obtain the IP address, it is in bytes 11 to 14 inclusive
                    ip_address = '%d.%d.%d.%d' % struct.unpack('>BBBB', response[11:15])
                    # obtain the port, it is in bytes 15 to 16 inclusive
                    port = struct.unpack('>H', response[15: 17])[0]
                    # if we haven't seen this ip address and port add them to
                    # our results list
                    if (ip_address, port) not in result_list:
                        result_list.append((ip_address, port))
            finally:
                # we are done so close our socket
                socket_obj.close()
            return result_list

        def get_livedata(self):
            """Get GW1000 live data."""

            return self.send_cmd_with_retries('CMD_GW1000_LIVEDATA')

        def get_raindata(self):
            """Get GW1000 rain data."""

            return self.send_cmd_with_retries('CMD_READ_RAINDATA')

        def get_system_params(self):
            """Read GW1000 system parameters."""

            return self.send_cmd_with_retries('CMD_READ_SSSS')

        def get_mac_address(self):
            """Get GW1000 MAC address."""

            return self.send_cmd_with_retries('CMD_READ_STATION_MAC')

        def get_firmware_version(self):
            """Get GW1000 firmware version."""

            return self.send_cmd_with_retries('CMD_READ_FIRMWARE_VERSION')

        def get_sensor_id(self):
            """Get GW1000 sensor ID data."""

            return self.send_cmd_with_retries('CMD_READ_SENSOR_ID')

        def send_cmd_with_retries(self, cmd):
            """Send a command to the GW1000 API with retries and return the
            response.

            Send a command to the GW1000 and obtain the response. If the
            the response is valid return the response. If the response is
            invalid an appropriate exception is raised and the command resent
            up to self.max_tries times after which the value None is returned.

            cmd: A string containing a valid GW1000 API command,
                 eg: 'CMD_READ_FIRMWARE_VERSION'

            Returns the response as a byte string or the value None.
            """

            # obtain the size of the command to be sent
            try:
                size = len(b''.join([self.header, self.commands[cmd]]))
            except KeyError:
                raise UnknownCommand("Unknown GW1000 API command '%s'" % (cmd,))
            # obtain the size of the header data being sent
            _header_data_size = b''.join([self.header, self.commands[cmd], struct.pack('B', size)])
            # calculate the checksum
            checksum = struct.pack('B', struct.unpack('B', self.commands[cmd])[0] + size)
            # construct the command packet
            packet = b''.join([_header_data_size, checksum])
            # attempt to send up to 'self.max_tries' times
            for attempt in range(self.max_tries):
                # wrap in  try..except so we can catch any errors
                try:
                    response = self.send_cmd(packet)
                except socket.timeout as e:
                    # a socket timeout occurred, log it then wait retry_wait
                    # seconds and continue
                    logdbg("Failed attempt %d to send command '%s': %s" % (attempt + 1, cmd, e))
                    time.sleep(self.retry_wait)
                except Exception as e:
                    # an exception was encountered, log it
                    logdbg("Failed attempt %d to send command '%s': %s" % (attempt + 1, cmd, e))
                else:
                    # if we made it here we have a response, check that it is
                    # valid
                    try:
                        self.check_response(response, self.commands[cmd])
                    except (InvalidChecksum, InvalidApiResponse) as e:
                        # the response was not valid, log it and attempt again
                        # if we haven't had too many attempts already
                        logdbg("Invalid response to attempt %d to send command '%s': %s" % (attempt + 1, cmd, e))
                    except Exception as e:
                        # Some other error occurred in check_response(),
                        # perhaps the response was malformed. Log the stack
                        # trace but continue.
                        logerr("Unexpected exception occurred while checking response "
                               "to attempt %d to send command '%s': %s" % (attempt + 1, cmd, e))
                        log_traceback_error('    ****  ')
                    else:
                        # our response is valid so return it
                        return response
            # if we made it here we failed after self.max_tries attempts, log it and return None
            logerr("Failed to send command '%s' after %d attempts" % (cmd, attempt + 1))
            return None

        def send_cmd(self, packet):
            """Send a command to the GW1000 API and return the response.

            Send a command to the GW1000 and return the response. Socket
            related errors are trapped and raised, code calling send_cmd should
            be prepared to handle such exceptions.

            cmd: A valid GW1000 API command

            Returns the response as a byte string.
            """

            # create socket objects for sending commands and broadcasting to the network
            socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            socket_obj.settimeout(self.socket_timeout)
            try:
                socket_obj.connect((self.ip_address, self.port))
                if weewx.debug >= 3:
                    logdbg("Sending packet '%s' to '%s:%d'" % (Gw1000Collector.bytes_to_hex(packet),
                                                               self.ip_address.decode(),
                                                               self.port))
                socket_obj.sendall(packet)
                response = socket_obj.recv(1024)
                if weewx.debug >= 3:
                    logdbg("Received response '%s'" % (Gw1000Collector.bytes_to_hex(response),))
                return response
            except socket.error:
                raise

        def check_response(self, response, cmd_code):
            """Check the validity of a GW1000 API response.

            Checks the validity of a GW1000 API response. Two checks are
            performed:

            1.  the third byte of the response is the same as the command code
                used in the API call
            2.  the calculated checksum of the data in the response matches the
                checksum byte in the response

            If any check fails an appropriate exception is raised, if all checks
            pass the method exits without raising an exception.

            response: Response received from the GW1000 API call. Byte string.
            cmd_code: Command code send to GW1000 API. Byte string of length
                      one.
            """

            # first check that the 3rd byte of the response is the command code that was issued
            if six.indexbytes(response, 2) == six.byte2int(cmd_code):
                # now check the checksum
                calc_checksum = self.calc_checksum(response[2:-1])
                resp_checksum = six.indexbytes(response, -1)
                if calc_checksum == resp_checksum:
                    # checksum check passed, response is deemed valid
                    return
                else:
                    # checksum check failed, raise an InvalidChecksum exception
                    _msg = "Invalid checksum in API response. "\
                           "Expected '%s' (0x%s), received '%s' (0x%s)." % (calc_checksum,
                                                                            "{:02X}".format(calc_checksum),
                                                                            resp_checksum,
                                                                            "{:02X}".format(resp_checksum))
                    raise InvalidChecksum(_msg)
            else:
                # command code check failed, raise an InvalidApiResponse exception
                exp_int = six.byte2int(cmd_code)
                resp_int = six.indexbytes(response, 2)
                _msg = "Invalid command code in API response. "\
                       "Expected '%s' (0x%s), received '%s' (0x%s)." % (exp_int,
                                                                        "{:02X}".format(exp_int),
                                                                        resp_int,
                                                                        "{:02X}".format(resp_int))
                raise InvalidApiResponse(_msg)

        @staticmethod
        def calc_checksum(data):
            """Calculate the checksum for a GW1000 API call or response.

            The checksum used on the GW1000 responses is simply the LSB of the
            sum of the bytes.

            data: The data on which the checksum is to be calculated. Byte
                  string.

            Returns the checksum as an integer.
            """

            # initialise the checksum to 0
            checksum = 0
            # iterate over each byte in the response
            for b in six.iterbytes(data):
                # add the byte to the running total
                checksum += b
            # we are only interested in the least significant byte
            return checksum % 256

    class Parser(object):
        """Class to parse GW1000 sensor data."""

        # Dictionary keyed by GW1000 response element containing various
        # parameters for each response 'field'. Dictionary tuple format
        # is (decode function name, size of data in bytes, GW1000 field name)
        response_struct = {
            b'\x01': ('decode_temp', 2, 'intemp'),
            b'\x02': ('decode_temp', 2, 'outtemp'),
            b'\x03': ('decode_temp', 2, 'dewpoint'),
            b'\x04': ('decode_temp', 2, 'windchill'),
            b'\x05': ('decode_temp', 2, 'heatindex'),
            b'\x06': ('decode_humid', 1, 'inhumid'),
            b'\x07': ('decode_humid', 1, 'outhumid'),
            b'\x08': ('decode_press', 2, 'absbarometer'),
            b'\x09': ('decode_press', 2, 'relbarometer'),
            b'\x0A': ('decode_dir', 2, 'winddir'),
            b'\x0B': ('decode_speed', 2, 'windspeed'),
            b'\x0C': ('decode_speed', 2, 'gustspeed'),
            b'\x0D': ('decode_rain', 2, 'rainevent'),
            b'\x0E': ('decode_rainrate', 2, 'rainrate'),
            b'\x0F': ('decode_rain', 2, 'rainhour'),
            b'\x10': ('decode_rain', 2, 'rainday'),
            b'\x11': ('decode_rain', 2, 'rainweek'),
            b'\x12': ('decode_big_rain', 4, 'rainmonth'),
            b'\x13': ('decode_big_rain', 4, 'rainyear'),
            b'\x14': ('decode_big_rain', 4, 'raintotals'),
            b'\x15': ('decode_light', 4, 'light'),
            b'\x16': ('decode_uv', 2, 'uv'),
            b'\x17': ('decode_uvi', 1, 'uvi'),
            b'\x18': ('decode_datetime', 6, 'datetime'),
            b'\x19': ('decode_speed', 2, 'daymaxwind'),
            b'\x1A': ('decode_temp', 2, 'temp1'),
            b'\x1B': ('decode_temp', 2, 'temp2'),
            b'\x1C': ('decode_temp', 2, 'temp3'),
            b'\x1D': ('decode_temp', 2, 'temp4'),
            b'\x1E': ('decode_temp', 2, 'temp5'),
            b'\x1F': ('decode_temp', 2, 'temp6'),
            b'\x20': ('decode_temp', 2, 'temp7'),
            b'\x21': ('decode_temp', 2, 'temp8'),
            b'\x22': ('decode_humid', 1, 'humid1'),
            b'\x23': ('decode_humid', 1, 'humid2'),
            b'\x24': ('decode_humid', 1, 'humid3'),
            b'\x25': ('decode_humid', 1, 'humid4'),
            b'\x26': ('decode_humid', 1, 'humid5'),
            b'\x27': ('decode_humid', 1, 'humid6'),
            b'\x28': ('decode_humid', 1, 'humid7'),
            b'\x29': ('decode_humid', 1, 'humid8'),
            b'\x2A': ('decode_aq', 2, 'pm251'),
            b'\x2B': ('decode_temp', 2, 'soiltemp1'),
            b'\x2C': ('decode_moist', 1, 'soilmoist1'),
            b'\x2D': ('decode_temp', 2, 'soiltemp2'),
            b'\x2E': ('decode_moist', 1, 'soilmoist2'),
            b'\x2F': ('decode_temp', 2, 'soiltemp3'),
            b'\x30': ('decode_moist', 1, 'soilmoist3'),
            b'\x31': ('decode_temp', 2, 'soiltemp4'),
            b'\x32': ('decode_moist', 1, 'soilmoist4'),
            b'\x33': ('decode_temp', 2, 'soiltemp5'),
            b'\x34': ('decode_moist', 1, 'soilmoist5'),
            b'\x35': ('decode_temp', 2, 'soiltemp6'),
            b'\x36': ('decode_moist', 1, 'soilmoist6'),
            b'\x37': ('decode_temp', 2, 'soiltemp7'),
            b'\x38': ('decode_moist', 1, 'soilmoist7'),
            b'\x39': ('decode_temp', 2, 'soiltemp8'),
            b'\x3A': ('decode_moist', 1, 'soilmoist8'),
            b'\x3B': ('decode_temp', 2, 'soiltemp9'),
            b'\x3C': ('decode_moist', 1, 'soilmoist9'),
            b'\x3D': ('decode_temp', 2, 'soiltemp10'),
            b'\x3E': ('decode_moist', 1, 'soilmoist10'),
            b'\x3F': ('decode_temp', 2, 'soiltemp11'),
            b'\x40': ('decode_moist', 1, 'soilmoist11'),
            b'\x41': ('decode_temp', 2, 'soiltemp12'),
            b'\x42': ('decode_moist', 1, 'soilmoist12'),
            b'\x43': ('decode_temp', 2, 'soiltemp13'),
            b'\x44': ('decode_moist', 1, 'soilmoist13'),
            b'\x45': ('decode_temp', 2, 'soiltemp14'),
            b'\x46': ('decode_moist', 1, 'soilmoist14'),
            b'\x47': ('decode_temp', 2, 'soiltemp15'),
            b'\x48': ('decode_moist', 1, 'soilmoist15'),
            b'\x49': ('decode_temp', 2, 'soiltemp16'),
            b'\x4A': ('decode_moist', 1, 'soilmoist16'),
            b'\x4C': ('decode_batt', 16, 'lowbatt'),
            b'\x4D': ('decode_aq', 2, '24havpm251'),
            b'\x4E': ('decode_aq', 2, '24havpm252'),
            b'\x4F': ('decode_aq', 2, '24havpm253'),
            b'\x50': ('decode_aq', 2, '24havpm254'),
            b'\x51': ('decode_aq', 2, 'pm252'),
            b'\x52': ('decode_aq', 2, 'pm253'),
            b'\x53': ('decode_aq', 2, 'pm254'),
            b'\x58': ('decode_leak', 1, 'leak1'),
            b'\x59': ('decode_leak', 1, 'leak2'),
            b'\x5A': ('decode_leak', 1, 'leak3'),
            b'\x5B': ('decode_leak', 1, 'leak4'),
            b'\x60': ('decode_distance', 1, 'lightningdist'),
            b'\x61': ('decode_utc', 4, 'lightningdettime'),
            b'\x62': ('decode_count', 4, 'lightningcount'),
            b'\x63': ('decode_temp_batt', 3, 'usertemp1'),
            b'\x64': ('decode_temp_batt', 3, 'usertemp2'),
            b'\x65': ('decode_temp_batt', 3, 'usertemp3'),
            b'\x66': ('decode_temp_batt', 3, 'usertemp4'),
            b'\x67': ('decode_temp_batt', 3, 'usertemp5'),
            b'\x68': ('decode_temp_batt', 3, 'usertemp6'),
            b'\x69': ('decode_temp_batt', 3, 'usertemp7'),
            b'\x6A': ('decode_temp_batt', 3, 'usertemp8'),
        }

        multi_batt = {'wh40': {'mask': 1 << 4},
                      'wh26': {'mask': 1 << 5},
                      'wh25': {'mask': 1 << 6},
                      'wh65': {'mask': 1 << 7}
                      }
        wh31_batt = {1: {'mask': 1 << 0},
                     2: {'mask': 1 << 1},
                     3: {'mask': 1 << 2},
                     4: {'mask': 1 << 3},
                     5: {'mask': 1 << 4},
                     6: {'mask': 1 << 5},
                     7: {'mask': 1 << 6},
                     8: {'mask': 1 << 7}
                     }
        wh41_batt = {1: {'shift': 0, 'mask': 0x0F},
                     2: {'shift': 4, 'mask': 0x0F},
                     3: {'shift': 8, 'mask': 0x0F},
                     4: {'shift': 12, 'mask': 0x0F}
                     }
        wh51_batt = {1: {'mask': 1 << 0},
                     2: {'mask': 1 << 1},
                     3: {'mask': 1 << 2},
                     4: {'mask': 1 << 3},
                     5: {'mask': 1 << 4},
                     6: {'mask': 1 << 5},
                     7: {'mask': 1 << 6},
                     8: {'mask': 1 << 7},
                     9: {'mask': 1 << 8},
                     10: {'mask': 1 << 9},
                     11: {'mask': 1 << 10},
                     12: {'mask': 1 << 11},
                     13: {'mask': 1 << 12},
                     14: {'mask': 1 << 13},
                     15: {'mask': 1 << 14},
                     16: {'mask': 1 << 15}
                     }
        wh55_batt = {1: {'shift': 0, 'mask': 0xFF},
                     2: {'shift': 8, 'mask': 0xFF},
                     3: {'shift': 16, 'mask': 0xFF},
                     4: {'shift': 24, 'mask': 0xFF}
                     }
        wh57_batt = {'wh57': {}}
        ws68_batt = {'ws68': {}}
        ws80_batt = {'ws80': {}}
        batt = {
            'multi': (multi_batt, 'battery_mask'),
            'wh31': (wh31_batt, 'battery_mask'),
            'wh51': (wh51_batt, 'battery_mask'),
            'wh41': (wh41_batt, 'battery_value'),
            'wh57': (wh57_batt, 'battery_value'),
            'ws68': (ws68_batt, 'battery_voltage'),
            'ws80': (ws80_batt, 'battery_voltage'),
            'wh55': (wh55_batt, 'battery_value'),
            'unused': ({}, 'battery_mask')
        }
        batt_fields = ('multi', 'wh31', 'wh51', 'wh57', 'ws68', 'ws80',
                       'unused', 'wh41', 'wh55')
        battery_state_format = "<BBHBBBBHLBB"

        def __init__(self, is_wh24=False):
            # Tell our battery state decoding whether we have a WH24 or a WH65
            # (they both share the same battery state bit). By default we are
            # coded to use a WH65. Is there a WH24 connected?
            if is_wh24:
                # there is a WH24 connected so create the WH24 decode dict
                # entry, it's the same as the WH65 decode entry
                self.multi_batt['wh24'] = self.multi_batt['wh65']
                # and pop off the no longer needed WH65 decode dict entry
                self.multi_batt.pop('wh65')

        def parse(self, raw_data, timestamp=None):
            """Parse raw sensor data.

            Parse the raw sensor data and create a dict of sensor
            observations/status data. Add a timestamp to the data if one does
            not already exist.

            Returns a dict of observations/status data."""

            # obtain the response size, it's a big endian short (two byte) integer
            resp_size = struct.unpack(">H", raw_data[3:5])[0]
            # obtain the response
            resp = raw_data[5:5 + resp_size - 4]
            # log the actual sensor data as a sequence of bytes in hex
            if weewx.debug >= 3:
                logdbg("sensor data is '%s'" % (Gw1000Collector.bytes_to_hex(resp),))
            if len(resp) > 0:
                index = 0
                data = {}
                while index < len(resp) - 1:
                    decode_str, field_size, field = self.response_struct[resp[index:index + 1]]
                    data.update(getattr(self, decode_str)(resp[index + 1:index + 1 + field_size],
                                                          field))
                    index += field_size + 1
            # if it does not exist add a datetime field with the current epoch timestamp
            if 'datetime' not in data or 'datetime' in data and data['datetime'] is None:
                data['datetime'] = timestamp if timestamp is not None else int(time.time() + 0.5)
            return data

        @staticmethod
        def decode_temp(data, field=None):
            """Decode temperature data.

            Data is contained in a two byte big endian signed integer and
            represents tenths of a degree.
            """

            if len(data) == 2:
                value = struct.unpack(">h", data)[0] / 10.0
            else:
                value = None
            if field is not None:
                return {field: value}
            else:
                return value

        @staticmethod
        def decode_humid(data, field=None):
            """Decode humidity data.

            Data is contained in a single unsigned byte and represents whole units.
            """

            if len(data) > 0:
                value = struct.unpack("B", data)[0]
            else:
                value = None
            if field is not None:
                return {field: value}
            else:
                return value

        @staticmethod
        def decode_press(data, field=None):
            """Decode pressure data.

            Data is contained in a two byte big endian integer and represents
            tenths of a unit.
            """

            if len(data) == 2:
                value = struct.unpack(">H", data)[0] / 10.0
            else:
                value = None
            if field is not None:
                return {field: value}
            else:
                return value

        @staticmethod
        def decode_big_rain(data, field=None):
            """Decode 4 byte rain data.

            Data is contained in a four byte big endian integer and represents
            tenths of a unit.
            """

            if len(data) >= 4:
                value = struct.unpack(">L", data)[0] / 10.0
            else:
                value = None
            if field is not None:
                return {field: value}
            else:
                return value

        @staticmethod
        def decode_datetime(data, field=None):
            """Decode date-time data.

            Unknown format but length is six bytes.
            """

            if len(data) >= 6:
                value = struct.unpack("BBBBBB", data)
            else:
                value = None
            if field is not None:
                return {field: value}
            else:
                return value

        def decode_temp_batt(self, data, field=None):
            """Decode combined temperature and battery status data.

            Data consists of three bytes; bytes 0 and 1 are normal temperature data
            and byte 3 is battery status data.
            """

            # do we have valid data
            if len(data) == 3:
                # yes, decode temperature from bytes 0 and 1
                temp = self.decode_temp(data[0:2], field)
                # decode battery voltage from byte 2
                batt = self.battery_voltage(data[2])
                # were we given a field to use for the return
                if field is not None:
                    # we have a field, 'temp' will be a dict so add the battery
                    # state data and return the resulting dict
                    temp['%s_batt' % field] = batt
                    return temp
                else:
                    # No field provided, so 'temp' will just be a value.
                    # Package temperature and battery state data in a generic
                    # dict and return
                    return {'temperature': temp,
                            'battery': batt
                            }
            else:
                # invalid data assumed, return None
                return None

        @staticmethod
        def decode_distance(data, field=None):
            """Decode lightning distance.

            Data is contained in a single byte integer and represents a value
            from 0 to 40km.
            """

            if len(data) >= 1:
                value = struct.unpack("B", data)[0]
                value = value if value <= 40 else None
            else:
                value = None
            if field is not None:
                return {field: value}
            else:
                return value

        @staticmethod
        def decode_utc(data, field=None):
            """Decode UTC time.

            GW1000 UTC times are seconds since Unix epoch and are stored in a
            4 byte big endian integer."""

            if len(data) >= 4:
                # unpack the 4 byte int
                value = struct.unpack(">L", data)[0]
                # when processing the last lightning strike time if the value
                # is 0xFFFFFFFF it means we have never seen a strike so return
                # None
                value = value if value != 0xFFFFFFFF else None
            else:
                resp = None
            if field is not None:
                return {field: value}
            else:
                return value

        @staticmethod
        def decode_count(data, field=None):
            """Decode lightning count.

            Count is an integer stored in a 4 byte big endian integer."""

            if len(data) >= 4:
                value = struct.unpack(">L", data)[0]
            else:
                value = None
            if field is not None:
                return {field: value}
            else:
                return value

        # alias' for other decodes
        decode_dir = decode_press
        decode_speed = decode_press
        decode_rain = decode_press
        decode_rainrate = decode_press
        decode_light = decode_big_rain
        decode_uv = decode_press
        decode_uvi = decode_humid
        decode_moist = decode_humid
        decode_aq = decode_press
        decode_leak = decode_humid

        def decode_batt(self, data, field):
            """Decode battery status data.

            Battery status data is provided in 16 bytes using a variety of
            representations. Different representations include:
            -   use of a single bit to indicate low/OK
            -   use of a nibble to indicate battery level
            -   use of a byte to indicate battery voltage

            WH24, WH25, WH26(WH32), WH31, WH40, WH41 and WH51
            stations/sensors use a single bit per station/sensor to indicate OK or
            low battery. WH55 and WH57 sensors use a single byte per sensor to
            indicate OK or low battery. WS68 and WS80 sensors use a single byte to
            store battery voltage.

            The battery status data is allocated as follows
            Byte #  Sensor          Value               Comments
            byte 1  WH40(b4)        0/1                 1=low, 0=normal
                    WH26(WH32?)(b5) 0/1                 1=low, 0=normal
                    WH25(b6)        0/1                 1=low, 0=normal
                    WH24(b7)        0/1                 may be WH65, 1=low, 0=normal
                 2  WH31 ch1(b0)    0/1                 1=low, 0=normal, 8 channels b0..b7
                         ...
                         ch8(b7)    0/1                 1=low, 0=normal
                 3  WH51 ch1(b0)    0/1                 1=low, 0=normal, 16 channels b0..b7 over 2 bytes
                         ...
                         ch8(b7)    0/1                 1=low, 0=normal
                 4       ch9(b0)    0/1                 1=low, 0=normal
                         ...
                         ch16(b7)   0/1                 1=low, 0=normal
                 5  WH57            0-5                 <=1 is low
                 6  WS68            0.02*value Volts
                 7  WS80            0.02*value Volts
                 8  Unused
                 9  WH41 ch1(b0-b3) 0-5                 <=1 is low
                         ch2(b4-b7) 0-5                 <=1 is low
                 10      ch3(b0-b3) 0-5                 <=1 is low
                         ch4(b4-b7) 0-5                 <=1 is low
                 11 WH55 ch1        0-5                 <=1 is low
                 12 WH55 ch2        0-5                 <=1 is low
                 13 WH55 ch3        0-5                 <=1 is low
                 14 WH55 ch4        0-5                 <=1 is low
                 15 Unused
                 16 Unused

            For stations/sensors using a single bit for battery status 0=OK and
            1=low. For stations/sensors using a single byte for battery
            status >1=OK and <=1=low. For stations/sensors using a single byte for
            battery voltage the voltage is 0.02 * the byte value.

                # WH24 F/O THWR sensor station
                # WH25 THP sensor
                # WH26(WH32) TH sensor
                # WH40 rain gauge sensor
            """

            if len(data) == 16:
                # first break out the bytes
                b_dict = {}
                batt_t = struct.unpack(self.battery_state_format, data)
                batt_dict = dict(six.moves.zip(self.batt_fields, batt_t))
                for field in self.batt_fields:
                    elements, decode_str = self.batt[field]
                    for elm in elements.keys():
                        # construct the field name for our battery value, how
                        # we construct the field name will depend if we have a
                        # numeric channel or not
                        # assume no numeric channel
                        try:
                            field_name = "".join([elm, '_batt'])
                        except TypeError:
                            # if we strike a TypeError it will be because we
                            # have a numeric channel number
                            field_name = ''.join([field, '_ch', str(elm), '_batt'])
                        # now add the battery value to the result dict
                        b_dict[field_name] = getattr(self, decode_str)(batt_dict[field],
                                                                       **elements[elm])
                return b_dict
            return {}

        @staticmethod
        def battery_mask(data, mask):
            if (data & mask) == mask:
                return 1
            return 0

        @staticmethod
        def battery_value(data, mask=None, shift=None):
            _data = data if shift is None else data >> shift
            if mask is not None:
                _value = _data & mask
                if _value == mask:
                    _value = None
            else:
                _value = _data
            return _value

        @staticmethod
        def battery_voltage(data):
            return 0.02 * data


# ============================================================================
#                             Utility functions
# ============================================================================

def natural_sort_dict(source_dict):
    """Return a naturally sorted list of keys for a dict."""

    def atoi(text):
        return int(text) if text.isdigit() else text

    def natural_keys(text):
        """Natural key sort.

        Allows use of key=natural_keys to sort a list in human order, eg:
            alist.sort(key=natural_keys)

        http://nedbatchelder.com/blog/200712/human_sorting.html (See
        Toothy's implementation in the comments)
        """

        return [atoi(c) for c in re.split(r'(\d+)', text)]

    # create a list of keys in the dict
    keys_list = list(source_dict.keys())
    # naturally sort the list of keys where, for example, xxxxx16 appears in the
    # correct order
    keys_list.sort(key=natural_keys)
    # return the sorted list
    return keys_list


# To use this driver in standalone mode for testing or development, use one of
# the following commands (depending on your WeeWX install). For setup.py
# installs use:
#
#   $ PYTHONPATH=/home/weewx/bin python -m user.gw1000
#
# or for package installs use:
#
#   $ PYTHONPATH=/usr/share/weewx python -m user.gw1000
#
# The above commands will display details of available command line options.
#
# Note. Whilst the driver may be run independently of WeeWX the driver still
# requires WeeWX and it's dependencies be installed. Consequently, if
# WeeWX 4.0.0 or later is installed the driver must be run under the same
# Python version as WeeWX uses. This means that on some systems 'python' in the
# above commands may need to be changed to 'python2' or 'python3'.

def main():
    import optparse

    def system_params(opts):
        """Display system parameters."""

        # dict for decoding system parameters frequency byte, at present all we
        # know is 0 = 433MHz
        freq_decode = {
            0: '433MHz',
            998: '868Mhz',
            999: '915MHz'
        }
        # obtain any command line specified ip address and port
        ip_address = opts.ip_address if opts.ip_address else None
        port = opts.port if opts.port else None
        # get a GW1000 Gw1000Collector object
        collector = Gw1000Collector(ip_address=ip_address,
                                    port=port)
        # identify the GW1000 being used
        print()
        print("Interrogating GW1000 at %s:%d" % (collector.station.ip_address.decode(),
                                                 collector.station.port))
        # get the collector objects system_parameters property, wrap in a try so
        # we can catch any socket timeouts
        try:
            sys_params_dict = collector.system_parameters
            # create a meaningful string for frequncy representation
            freq_str = freq_decode.get(sys_params_dict['frequency'], 'Unknown')
            # if sensor_type is 0 there is a WH24 connected, if its a 1 there
            # is a WH65
            _is_wh24 = sys_params_dict['sensor_type'] == 0
            # string to use in sensor type message
            _sensor_type_str = 'WH24' if _is_wh24 else 'WH65'
        except socket.timeout:
            # socket timeout so inform the user
            print()
            print("Timeout. GW1000 did not respond.")
        else:
            # print the system parameters
            print()
            print("GW1000 frequency: %s (%s)" % (sys_params_dict['frequency'],
                                                 freq_str))
            print("GW1000 sensor type: %s (%s)" % (sys_params_dict['sensor_type'],
                                                   _sensor_type_str))
            print("GW1000 decoded UTC: %s" % weeutil.weeutil.timestamp_to_gmtime(sys_params_dict['utc']))
            print("GW1000 timezone: %s" % (sys_params_dict['timezone'],))

    def rain_data(opts):
        """Display rain data."""

        # obtain any command line specified ip address and port
        ip_address = opts.ip_address if opts.ip_address else None
        port = opts.port if opts.port else None
        # get a GW1000 Gw1000Collector object
        collector = Gw1000Collector(ip_address=ip_address,
                                    port=port)
        # identify the GW1000 being used
        print()
        print("Interrogating GW1000 at %s:%d" % (collector.station.ip_address.decode(),
                                                 collector.station.port))
        # get the collector objects rain_data property, wrap in a try so we can
        # catch any socket timeouts
        try:
            rain_data = collector.rain_data
        except socket.timeout:
            print()
            print("Timeout. GW1000 did not respond.")
        else:
            print()
            print("%10s: %.1f mm/%.1f in" % ('Rain rate', rain_data['rain_rate'], rain_data['rain_rate']/25.4))
            print("%10s: %.1f mm/%.1f in" % ('Day rain', rain_data['rain_day'], rain_data['rain_day']/25.4))
            print("%10s: %.1f mm/%.1f in" % ('Week rain', rain_data['rain_week'], rain_data['rain_week']/25.4))
            print("%10s: %.1f mm/%.1f in" % ('Month rain', rain_data['rain_month'], rain_data['rain_month']/25.4))
            print("%10s: %.1f mm/%.1f in" % ('Year rain', rain_data['rain_year'], rain_data['rain_year']/25.4))

    def station_mac(opts):

        ip_address = opts.ip_address if opts.ip_address else None
        port = opts.port if opts.port else None
        # get a GW1000 Gw1000Collector object
        collector = Gw1000Collector(ip_address=ip_address,
                                    port=port)
        # identify the GW1000 being used
        print()
        print("Interrogating GW1000 at %s:%d" % (collector.station.ip_address.decode(),
                                                 collector.station.port))
        # call the driver objects mac_address() method, wrap in a try so
        # we can catch any socket timeouts
        try:
            print()
            print("GW1000 MAC address: %s" % (collector.mac_address, ))
        except socket.timeout:
            print()
            print("Timeout. GW1000 did not respond.")

    def firmware(opts):

        ip_address = opts.ip_address if opts.ip_address else None
        port = opts.port if opts.port else None
        # get a Gw1000Collector object
        collector = Gw1000Collector(ip_address=ip_address,
                                    port=port)
        # identify the GW1000 being used
        print()
        print("Interrogating GW1000 at %s:%d" % (collector.station.ip_address.decode(),
                                                 collector.station.port))
        # call the driver objects firmware_version() method, wrap in a try so
        # we can catch any socket timeouts
        try:
            print()
            print("GW1000 firmware version string: %s" % (collector.firmware_version, ))
        except socket.timeout:
            print()
            print("Timeout. GW1000 did not respond.")

    def sensors(opts):

        ip_address = opts.ip_address if opts.ip_address else None
        port = opts.port if opts.port else None
        # get a Gw1000Collector object
        collector = Gw1000Collector(ip_address=ip_address,
                                    port=port)
        # identify the GW1000 being used
        print()
        print("Interrogating GW1000 at %s:%d" % (collector.station.ip_address.decode(),
                                                 collector.station.port))
        # call the driver objects get_sensor_ids() method, wrap in a try so we
        # can catch any socket timeouts
        try:
            sensor_id_data = collector.sensor_id_data
        except socket.timeout:
            print()
            print("Timeout. GW1000 did not respond.")
            return
        # print("GW1000 sensor ID data: %s" % (sensor_id_data, ))
        # now format and display the data
        print()
        print("%-10s %s" % ("Sensor", "Status"))
        # iterate over each sensor for which we have data
        for sensor in sensor_id_data:
            # sensor address
            address = sensor['address']
            # the sensor id indicates whether it is disabled, attempting to
            # register a sensor or already registered
            if sensor.get('id') == 'fffffffe':
                state = 'sensor is disabled'
            elif sensor.get('id') == 'ffffffff':
                state = 'sensor is registering...'
            else:
                # the sensor is registered so we should have signal and battery
                # data as well
                state = "sensor ID: %s  signal: %s  battery: %s" % (sensor.get('id').strip('0'),
                                                                    sensor.get('signal'),
                                                                    sensor.get('battery'))
            # print the formatted data
            print("%-10s %s" % (Gw1000Collector.sensor_ids[address].get('long_name'), state))

    def live_data(opts):

        ip_address = opts.ip_address if opts.ip_address else None
        port = opts.port if opts.port else None
        # get a Gw1000Collector object
        collector = Gw1000Collector(ip_address=ip_address,
                                    port=port)
        # identify the GW1000 being used
        print()
        print("Interrogating GW1000 at %s:%d" % (collector.station.ip_address.decode(),
                                                 collector.station.port))
        # call the driver objects get_live_sensor_data() method, wrap in a try
        # so we can catch any socket timeouts
        try:
            live_sensor_data_dict = collector.get_live_sensor_data()
        except socket.timeout:
            print()
            print("Timeout. GW1000 did not respond.")
        else:
            print()
            print("GW1000 live sensor data: %s" % weeutil.weeutil.to_sorted_string(live_sensor_data_dict))

    def discover():

        # get an Gw1000Collector object
        collector = Gw1000Collector()
        # call the Gw1000Collector object discover() method, wrap in a try so we can
        # catch any socket timeouts
        print()
        try:
            ip_port_list = collector.station.discover()
        except socket.timeout:
            print("Timeout. No GW1000 discovered.")
        else:
            if len(ip_port_list) > 0:
                # we have at least one result
                # first sort our list by IP address
                sorted_list = sorted(ip_port_list, key=itemgetter(0))
                found = False
                gw1000_found = 0
                for (ip, port) in sorted_list:
                    if ip is not None and port is not None:
                        print("GW1000 discovered at IP address %s on port %d" % (ip, port))
                        found = True
                        gw1000_found += 1
                else:
                    if gw1000_found > 1:
                        print()
                        print("Multiple GW1000 were found.")
                        print("If using the GW1000 driver consider explicitly specifying the IP address")
                        print("and port of the GW1000 to be used under [GW1000] in weewx.conf.")
                    if not found:
                        print("No GW1000 was discovered.")
            else:
                # we have no results
                print("No GW1000 was discovered.")

    def field_map():
        """Display the default field map."""

        print()
        print("GW1000 driver/service default field map:")
        print("(format is WeeWX field name: GW1000 field name)")
        print()
        # obtain a list of naturally sorted dict keys so that, for example,
        # xxxxx16 appears in the correct order
        keys_list = natural_sort_dict(Gw1000.default_field_map)
        # iterate over the sorted keys and print the key and item
        for key in keys_list:
            print("    %s: %s" % (key, Gw1000.default_field_map[key]))

    def test_driver(opts):
        """Run the GW1000 driver."""

        loginf("Testing GW1000 driver...")
        stn_dict = dict()
        if opts.ip_address:
            stn_dict['ip_address'] = opts.ip_address
        if opts.port:
            stn_dict['port'] = opts.port
        if opts.poll_interval:
            stn_dict['poll_interval'] = opts.poll_interval
        if opts.max_tries:
            stn_dict['max_tries'] = opts.max_tries
        if opts.retry_wait:
            stn_dict['retry_wait'] = opts.retry_wait
        # get a Gw1000Driver object
        driver = Gw1000Driver(**stn_dict)
        # identify the GW1000 being used
        print()
        print("Interrogating GW1000 at %s:%d" % (driver.collector.station.ip_address.decode(),
                                                 driver.collector.station.port))
        print()
        # wrap in a try..except so we can pickup a keyboard interrupt
        try:
            # continuously get loop packets and print them to screen
            for pkt in driver.genLoopPackets():
                print(": ".join([weeutil.weeutil.timestamp_to_string(pkt['dateTime']),
                                 weeutil.weeutil.to_sorted_string(pkt)]))
        except KeyboardInterrupt:
            # we have a keyboard interrupt so shut down
            driver.closePort()
        loginf("GW1000 driver testing complete")

    def test_service(opts):
        """Test the GW1000 service.

        Uses a dummy engine/simulator to generate arbitrary loop packets for
        augmenting. Use a 10 second loop interval so we don't get too many bare
        packets.
        """

        loginf("Testing GW1000 service...")
        # Create a dummy config so we can stand up a dummy engine with a dummy
        # simulator emitting arbitrary loop packets. Include the GW1000 service
        # and StdPrint, StdPrint will take care of printing our loop packets
        # (no StdArchive so loop packets only, no archive records)
        config = {
            'Station': {
                'station_type': 'Simulator',
                'altitude': [0, 'meter'],
                'latitude': 0,
                'longitude': 0},
            'Simulator': {
                'driver': 'weewx.drivers.simulator',
                'mode': 'simulator'},
            'Gw1000Service': {},
            'Engine': {
                'Services': {
                    'archive_services': 'user.gw1000.Gw1000Service',
                    'report_services': 'weewx.engine.StdPrint'}}}
        # these command line options should only be added if they exist
        if opts.ip_address:
            config['Gw1000Service']['ip_address'] = opts.ip_address
        if opts.port:
            config['Gw1000Service']['port'] = opts.port
        if opts.poll_interval:
            config['Gw1000Service']['poll_interval'] = opts.poll_interval
        if opts.max_tries:
            config['Gw1000Service']['max_tries'] = opts.max_tries
        if opts.retry_wait:
            config['Gw1000Service']['retry_wait'] = opts.retry_wait
        # assign our dummyTemp field to a unit group so unit conversion works
        # properly
        weewx.units.obs_group_dict['dummyTemp'] = 'group_temperature'
        # create a dummy engine
        engine = weewx.engine.StdEngine(config)
        # Our GW1000 service will have been instantiated by the engine during
        # its startup. Whilst access to the service is not normally required we
        # require access here so we can obtain some info about the station we
        # are using for this test. The engine does not provide a ready means to
        # access that GW1000 service so we can do a bit of guessing and iterate
        # over all of the engine's services and select the one that has a
        # 'collector' property. Unlikely to cause a problem since there are
        # only two services in the dummy engine.
        gw1000_svc = None
        for svc in engine.service_obj:
            if hasattr(svc, 'collector'):
                gw1000_svc = svc
        if gw1000_svc is not None:
            # identify the GW1000 being used
            print()
            print("Interrogating GW1000 at %s:%d" % (gw1000_svc.collector.station.ip_address.decode(),
                                                     gw1000_svc.collector.station.port))
        print()
        try:
            while True:
                # create an arbitrary loop packet, all it needs is a timestamp, a
                # defined unit system and a token obs
                packet = {'dateTime': int(time.time()),
                          'usUnits': weewx.US,
                          'dummyTemp': 96.3
                          }
                # send out a NEW_LOOP_PACKET event with the dummy loop packet
                # to trigger the GW1000 service to augment the loop packet
                engine.dispatchEvent(weewx.Event(weewx.NEW_LOOP_PACKET,
                                                 packet=packet,
                                                 origin='software'))
                # sleep for a bit to emulate the simulator
                time.sleep(10)
        except KeyboardInterrupt:
            engine.shutDown()
        loginf("GW1000 service testing complete")

    usage = """Usage: python -m user.gw1000 --help
       python -m user.gw1000 --version
       python -m user.gw1000 --test-driver
            [CONFIG_FILE|--config=CONFIG_FILE]  
            [--ip=IP_ADDRESS] [--port=PORT]
            [--poll-interval=INTERVAL]
            [--max-tries=MAX_TRIES]
            [--retry-wait=RETRY_WAIT]
            [--debug=0|1|2|3]     
       python -m user.gw1000 --test-service
            [CONFIG_FILE|--config=CONFIG_FILE]  
            [--ip=IP_ADDRESS] [--port=PORT]
            [--poll-interval=INTERVAL]
            [--max-tries=MAX_TRIES]
            [--retry-wait=RETRY_WAIT]
            [--debug=0|1|2|3]     
       python -m user.gw1000 --firmware-version
            [CONFIG_FILE|--config=CONFIG_FILE]  
            [--ip=IP_ADDRESS] [--port=PORT]
            [--debug=0|1|2|3]     
       python -m user.gw1000 --mac-address
            [CONFIG_FILE|--config=CONFIG_FILE]  
            [--ip=IP_ADDRESS] [--port=PORT]
            [--debug=0|1|2|3]     
       python -m user.gw1000 --sensors
            [CONFIG_FILE|--config=CONFIG_FILE]
            [--ip=IP_ADDRESS] [--port=PORT]
            [--debug=0|1|2|3]     
       python -m user.gw1000 --live-data
            [CONFIG_FILE|--config=CONFIG_FILE]  
            [--ip=IP_ADDRESS] [--port=PORT]
            [--debug=0|1|2|3]     
       python -m user.gw1000 --system-params
            [CONFIG_FILE|--config=CONFIG_FILE]  
            [--ip=IP_ADDRESS] [--port=PORT]
            [--debug=0|1|2|3]     
       python -m user.gw1000 --rain-data
            [CONFIG_FILE|--config=CONFIG_FILE]  
            [--ip=IP_ADDRESS] [--port=PORT]
            [--debug=0|1|2|3]     
       python -m user.gw1000 --discover
            [CONFIG_FILE|--config=CONFIG_FILE]  
            [--debug=0|1|2|3]"""

    parser = optparse.OptionParser(usage=usage)
    parser.add_option('--version', dest='version', action='store_true',
                      help='display GW1000 driver version number')
    parser.add_option('--config', dest='config_path', metavar='CONFIG_FILE',
                      help="Use configuration file CONFIG_FILE.")
    parser.add_option('--debug', dest='debug', type=int,
                      help='How much status to display, 0-3')
    parser.add_option('--discover', dest='discover', action='store_true',
                      help='discover GW1000 and display its IP address '
                           'and port')
    parser.add_option('--firmware-version', dest='firmware', action='store_true',
                      help='display GW1000 firmware version')
    parser.add_option('--mac-address', dest='mac', action='store_true',
                      help='display GW1000 station MAC address')
    parser.add_option('--system-params', dest='sys_params', action='store_true',
                      help='display GW1000 system parameters')
    parser.add_option('--sensors', dest='sensors', action='store_true',
                      help='display GW1000 sensor information')
    parser.add_option('--live-data', dest='live', action='store_true',
                      help='display GW1000 sensor data')
    parser.add_option('--rain-data', dest='rain', action='store_true',
                      help='display GW1000 rain data')
    parser.add_option('--default-map', dest='map', action='store_true',
                      help='display the default field map')
    parser.add_option('--test-driver', dest='test_driver', action='store_true',
                      metavar='TEST_DRIVER', help='test the GW1000 driver')
    parser.add_option('--test-service', dest='test_service', action='store_true',
                      metavar='TEST_SERVICE', help='test the GW1000 service')
    parser.add_option('--ip', dest='ip_address',
                      help='GW1000 IP address to use')
    parser.add_option('--port', dest='port', type=int,
                      help='GW1000 port to use')
    parser.add_option('--poll-interval', dest='poll_interval', type=int,
                      help='GW1000 port to use')
    parser.add_option('--max-tries', dest='max_tries', type=int,
                      help='GW1000 port to use')
    parser.add_option('--retry-wait', dest='retry_wait', type=int,
                      help='GW1000 port to use')
    (opts, args) = parser.parse_args()

    # get config_dict to use
    config_path, config_dict = weecfg.read_config(opts.config_path, args)
    print("Using configuration file %s" % config_path)
    stn_dict = config_dict.get('GW1000', {})

    # set weewx.debug as necessary
    if opts.debug is not None:
        _debug = weeutil.weeutil.to_int(opts.debug)
    else:
        _debug = weeutil.weeutil.to_int(config_dict.get('debug', 0))
    weewx.debug = _debug

    # Now we can set up the user customized logging but we need to handle both
    # v3 and v4 logging. V4 logging is very easy but v3 logging requires us to
    # set up syslog and raise our log level based on weewx.debug
    try:
        # assume v 4 logging
        weeutil.logger.setup('weewx', config_dict)
    except AttributeError:
        # must be v3 logging, so first set the defaults for the system logger
        syslog.openlog('weewx', syslog.LOG_PID | syslog.LOG_CONS)
        # now raise the log level if required
        if weewx.debug > 0:
            syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_DEBUG))

    # display driver version number
    if opts.version:
        print("%s driver version: %s" % (DRIVER_NAME, DRIVER_VERSION))
        exit(0)

    # run the driver
    if opts.test_driver:
        test_driver(opts)
        exit(0)

    # run the service with simulator
    if opts.test_service:
        test_service(opts)
        exit(0)

    if opts.sys_params:
        system_params(opts)
        exit(0)

    if opts.rain:
        rain_data(opts)
        exit(0)

    if opts.mac:
        station_mac(opts)
        exit(0)

    if opts.firmware:
        firmware(opts)
        exit(0)

    if opts.sensors:
        sensors(opts)
        exit(0)

    if opts.live:
        live_data(opts)
        exit(0)

    if opts.discover:
        discover()
        exit(0)

    if opts.map:
        field_map()
        exit(0)

    # if we made it here no option was selected so display our help
    parser.print_help()

if __name__ == '__main__':

    main()