Hi Tom: Attached is the 11rc3 version of ultimeter.py

Steve

-- 
You received this message because you are subscribed to the Google Groups 
"weewx-user" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to weewx-user+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
#!/usr/bin/env python
# $Id: ultimeter.py 2842 2015-01-10 16:12:02Z mwall $
# Copyright 2014 Matthew Wall
# Copyright 2014 Nate Bargmann <n...@n0nb.us>
# See the file LICENSE.txt for your full rights.
#
# Credit to and contributions from:
#   Jay Nugent (WB8TKL) and KRK6 for weather-2.kr6k-V2.1
#     http://server1.nuge.com/~weather/
#   Steve (sesykes71) for testing the first implementations of this driver
#   Garret Power for decoding improvements and testing

"""Driver for Peet Bros Ultimeter weather stations except the Ultimeter II

This driver assumes the Ultimeter is emitting data in Peet Bros Data Logger
mode format.

Resources for the Ultimeter stations

Ultimeter Models 2100, 2000, 800, & 100 serial specifications:
  http://www.peetbros.com/shop/custom.aspx?recid=29

Ultimeter 2000 Pinouts and Parsers:
  http://www.webaugur.com/ham-radio/52-ultimeter-2000-pinouts-and-parsers.html

Ultimeter II
  not supported by this driver

All models communicate over an RS-232 compatible serial port using three
wires--RXD, TXD, and Ground (except Ultimeter II which omits TXD).  Port
parameters are 2400, 8N1, with no flow control.

The Ultimeter hardware supports several "modes" for providing station data
to the serial port.  This driver utilizes the "modem mode" to set the date
and time of the Ultimeter upon initialization and then sets it into Data
Logger mode for continuous updates.

Modem Mode commands used by the driver
    >Addddmmmm  Set Date and Time (decimal digits dddd = day of year,
                mmmm = minute of day; Jan 1 = 0000, Midnight = 0000)

    >I          Set output mode to Data Logger Mode (continuous output)

"""

from __future__ import with_statement
import serial
import syslog
import time

import weewx.drivers

DRIVER_NAME = 'Ultimeter'
DRIVER_VERSION = '0.11rc3'

INHG_PER_MBAR = 0.0295333727
METER_PER_FOOT = 0.3048
MILE_PER_KM = 0.621371

def loader(config_dict, engine):
    return Ultimeter(**config_dict[DRIVER_NAME])

def confeditor_loader():
    return UltimeterConfEditor()


DEFAULT_PORT = '/dev/ttyS0'
DEBUG_READ = 0

def logmsg(level, msg):
    syslog.syslog(level, 'ultimeter: %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)


class Ultimeter(weewx.drivers.AbstractDevice):
    """weewx driver that communicates with a Peet Bros Ultimeter station

    model: station model, e.g., 'Ultimeter 2000' or 'Ultimeter 100'
    [Optional. Default is 'Ultimeter']

    port - serial port
    [Required. Default is /dev/ttyS0]

    polling_interval - how often to query the serial interface, seconds
    [Optional. Default is 1]

    max_tries - how often to retry serial communication before giving up
    [Optional. Default is 5]
    """
    def __init__(self, **stn_dict):
        self.model = stn_dict.get('model', 'Ultimeter')
        self.port = stn_dict.get('port', DEFAULT_PORT)
        self.polling_interval = float(stn_dict.get('polling_interval', 1))
        self.max_tries = int(stn_dict.get('max_tries', 5))
        self.retry_wait = int(stn_dict.get('retry_wait', 10))
        self.last_rain = None
        loginf('driver version is %s' % DRIVER_VERSION)
        loginf('using serial port %s' % self.port)
        loginf('polling interval is %s' % str(self.polling_interval))
        global DEBUG_READ
        DEBUG_READ = int(stn_dict.get('debug_read', DEBUG_READ))

    def genLoopPackets(self):
        ntries = 0
        while ntries < self.max_tries:
            ntries += 1
            try:
                packet = {'dateTime': int(time.time() + 0.5),
                          'usUnits': weewx.US}
                # open a new connection to the station for each reading
                with Station(self.port) as station:
                    readings = station.get_readings()
                data = Station.parse_readings(readings)
                packet.update(data)
                self._augment_packet(packet)
                ntries = 0
                yield packet
                if self.polling_interval:
                    time.sleep(self.polling_interval)
            except (serial.serialutil.SerialException, weewx.WeeWxIOError), e:
                logerr("Failed attempt %d of %d to get LOOP data: %s" %
                       (ntries, self.max_tries, e))
                time.sleep(self.retry_wait)
        else:
            msg = "Max retries (%d) exceeded for LOOP data" % self.max_tries
            logerr(msg)
            raise weewx.RetriesExceeded(msg)

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

    def _augment_packet(self, packet):
        # calculate the rain
        if self.last_rain is not None:
            packet['rain'] = packet['long_term_rain'] - self.last_rain
        else:
            packet['rain'] = None
        self.last_rain = packet['long_term_rain']

        # no wind direction when wind speed is zero
        if not packet['windSpeed']:
            packet['windDir'] = None

def _is_valid_char(c):
    """See whether a character is a valid hexadecimal digit or hyphen."""
    if c == '-':
        return True
    try:
        int(c, 16)
        return True
    except ValueError:
        return False

def _decode(s, multiplier=None, neg=False):
    """Ultimeter puts hyphens in the string when a sensor is not installed.
    When we get a hyphen or any other non-hex character, return None.
    Negative values are represented in twos complement format.  Only do the
    check for negative values if requested - the neg flag indicates that
    negative values are possible.
    """
    v = None
    try:
        v = int(s, 16)
        if neg:
            bits = 4 * len(s)
            if v & (1<<(bits-1)) != 0:
                v = v - (1<<bits)
        if multiplier is not None:
            v *= multiplier
    except ValueError:
        pass
    return v

class Station(object):
    def __init__(self, port):
        self.port = port
        self.baudrate = 2400
        self.timeout = 30
        self.serial_port = None

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

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

    def open(self):
        logdbg("open serial port %s" % self.port)
        self.serial_port = serial.Serial(self.port, self.baudrate,
                                         timeout=self.timeout)

        # Set date and time as internal clock skews.
        self.serial_port.write(">A%04d%04d\r"
            % (time.localtime().tm_yday - 1, time.localtime().tm_min
            + time.localtime().tm_hour * 60))

        # Set to Data Logger Mode
        self.serial_port.write(">I\r")

    def close(self):
        if self.serial_port is not None:
            logdbg("close serial port %s" % self.port)

            # Set to Modem Mode (stops Data Logger output)
            self.serial_port.write(">\r")

            self.serial_port.close()
            self.serial_port = None

    def read(self, nchar=1):
        buf = self.serial_port.read(nchar)
        n = len(buf)
        if n != nchar:
            if DEBUG_READ:
                logdbg("partial buffer: '%s'" %
                       ' '.join(["%0.2X" % ord(c) for c in buf]))
            raise weewx.WeeWxIOError("Read expected %d chars, got %d" %
                                     (nchar, n))
        return buf

    def write(self, data):
        n = self.serial_port.write(data)
        if n is not None and n != len(data):
            raise weewx.WeeWxIOError("Write expected %d chars, sent %d" %
                                     (len(data), n))

    def get_readings(self):
        buf = []
        while True:
            c = self.read(1)
            if c == "\r" or c == "\n":
                break
            elif c == '!' and len(buf) > 0:
                break
            elif c == '!':
                buf = []
            elif _is_valid_char(c):
                buf.append(c)
            else:
                buf = []
        if DEBUG_READ:
            logdbg("bytes: '%s'" % ' '.join(["%0.2X" % ord(c) for c in buf]))
        if len(buf) != 48:
            raise weewx.WeeWxIOError("Got %d bytes, expected 48" % len(buf))
        return ''.join(buf)

    @staticmethod
    def parse_readings(buf):
        """Ultimeter stations emit data in PeetBros format.  Each line has 52
        characters - 2 header bytes, 48 data bytes, and a carriage return
        and line feed (new line):

        !!000000BE02EB000027700000023A023A0025005800000000\r\n
          SSSSXXDDTTTTLLLLPPPPttttHHHHhhhhddddmmmmRRRRWWWW

          SSSS - wind speed (0.1 kph)
          XX   - wind direction calibration
          DD   - wind direction (0-255)
          TTTT - outdoor temperature (0.1 F)
          LLLL - long term rain (0.01 in)
          PPPP - pressure (0.1 mbar)
          tttt - indoor temperature (0.1 F)
          HHHH - outdoor humidity (0.1 %)
          hhhh - indoor humidity (0.1 %)
          dddd - date (day of year)
          mmmm - time (minute of day)
          RRRR - daily rain (0.01 in)
          WWWW - one minute wind average (0.1 kph)

        "pressure" reported by the Ultimeter 2000 is correlated to the local
        official barometer reading as part of the setup of the station
        console so this value is assigned to the 'barometer' key and
        the pressure and altimeter values are calculated from it.
        """
        data = dict()
        data['windSpeed'] = _decode(buf[0:4], 0.1 * MILE_PER_KM)  # mph
        data['windDir'] = _decode(buf[6:8], 1.411764)  # compass degrees
        data['outTemp'] = _decode(buf[8:12], 0.1, neg=True)  # degree_F
        data['long_term_rain'] = _decode(buf[12:16], 0.01)  # inch
        data['barometer'] = _decode(buf[16:20], 0.1 * INHG_PER_MBAR)  # inHg
        data['inTemp'] = _decode(buf[20:24], 0.1, neg=True)  # degree_F
        data['outHumidity'] = _decode(buf[24:28], 0.1)  # percent
        data['inHumidity'] = _decode(buf[28:32], 0.1)  # percent
        data['day_of_year'] = _decode(buf[32:36])
        data['minute_of_day'] = _decode(buf[36:40])
        data['daily_rain'] = _decode(buf[40:44], 0.01)  # inch
        data['wind_average'] = _decode(buf[44:48], 0.1 * MILE_PER_KM)  # mph
        return data


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

    # Serial port such as /dev/ttyS0, /dev/ttyUSB0, or /dev/cuaU0
    port = /dev/ttyUSB0

    # The station model, e.g., Ultimeter 2000, Ultimeter 100
    model = Ultimeter

    # The driver to use:
    driver = weewx.drivers.ultimeter
"""

    def prompt_for_settings(self):
        print "Specify the serial port on which the station is connected, for"
        print "example /dev/ttyUSB0 or /dev/ttyS0."
        port = self._prompt('port', '/dev/ttyUSB0')
        return {'port': port}


# define a main entry point for basic testing of the station without weewx
# engine and service overhead.  invoke this as follows from the weewx root dir:
#
# PYTHONPATH=bin python bin/weewx/drivers/ultimeter.py

if __name__ == '__main__':
    import optparse

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

    syslog.openlog('ultimeter', syslog.LOG_PID | syslog.LOG_CONS)
    syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_DEBUG))
    parser = optparse.OptionParser(usage=usage)
    parser.add_option('--version', dest='version', action='store_true',
                      help='display driver version')
    parser.add_option('--port', dest='port', metavar='PORT',
                      help='serial port to which the station is connected',
                      default=DEFAULT_PORT)
    (options, args) = parser.parse_args()

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

    with Station(options.port) as s:
        print s.get_readings()

Reply via email to