Replace your copy of accum.py with the attached. Should be at
/home/weewx/bin/weewx/accum.py. The new version has been instrumented to
check on how txBatteryStatus is being calculated from the LOOP packets.

Then restart weewx, wait until after the first reporting cycle, then post
the log from startup.

Oh, one other question: what is option [StdArchive] / record_generation set
to? 'hardware' or 'software' ?

-tk


On Fri, Nov 15, 2019 at 9:33 AM Geoff Cusick <ge...@cusick.org.uk> wrote:

> Thanks Tom.
>
> I stopped weewx (sudo service weewx stop), then started it from the
> command line, as per the manual.
>
> The LOOP packets all show txBatteryStatus as 1 - not surprising, as I only
> replaced the battery a couple of hours ago.  But the REC packets show
>  txBatteryStatus as None.
>
> So it looks as if txBatteryStatus is not being copied from the loop
> packets.
>
> Regards
> Geoff
>
> 17 Westcroft Road Holsworthy, EX22 6BY
> Phone: +44 (0)1409 254330
> Mobile: +44 (0)7976 285950
> Web: http://www.cusick.org.uk
>
> On 15 Nov 2019, at 17:17, Thomas Keffer <tkef...@gmail.com> wrote:
>
> 
> One thing you could try is running weewx from the command line
> <http://weewx.com/docs/usersguide.htm#Running_directly>. It will print
> out to the console all the data in an archive record. Watch for entries
> marked "REC" (most will be marked "LOOP"). What is the value of
> txBatteryStatus in there?
>
> -tk
>
> On Fri, Nov 15, 2019 at 9:13 AM Geoff Cusick <ge...@cusick.org.uk> wrote:
>
>> Thanks for the (very) speedy response, Tom.
>>
>> I’m running weewx 3.9.2 on a Raspberry Pi, raspbian buster.  I’m using a
>> MySQL database, hosted on a Synology NAS (MariaDB 5).  Looking at the
>> archive table directly through phpmyadmin shows txBatteryStatus as
>> constantly NULL.  A query:
>>
>> select * FROM  ‘archive’ WHERE txBatteryStatus <> NULL;
>>
>> returns no records.
>>
>> Everything else seems to work correctly (see weather.cusick.org.uk);
>> having just had to replace the ISS battery, I thought I’d add the battery
>> status to the ‘About’ page.
>>
>> Your help would be much appreciated.
>>
>> Geoff
>>
>> 17 Westcroft Road Holsworthy, EX22 6BY
>> Phone: +44 (0)1409 254330
>> Mobile: +44 (0)7976 285950
>> Web: http://www.cusick.org.uk
>>
>> On 15 Nov 2019, at 16:57, Thomas Keffer <tkef...@gmail.com> wrote:
>>
>> 
>> Yes, both have been emitted since v2.6.3 (released 10-Apr-2014).
>>
>> Is txBatteryStatus null, or zero? It's normally zero. If it is truly
>> null, then something is wrong.
>>
>> On Fri, Nov 15, 2019 at 8:07 AM Geoff Cusick <ge...@cusick.org.uk> wrote:
>>
>>> I know this is an old question.....
>>>
>>> Has the change that Tom mentioned been incorporated in later versions of
>>> vantage.py?  I’m using weewx v 3.9.2, but txBatteryStatus is always NULL in
>>> the database.
>>>
>>> I’m using a Davis VantageVue.
>>>
>>> Thanks
>>> Geoff
>>>
>>> On Monday, 24 February 2014 13:45:43 UTC, Thomas Keffer wrote:
>>>>
>>>> This is a reasonable thing to want and nearly trivial to add. Here's a
>>>> version of the Vantage driver that saves the last value of txBatteryStatus
>>>> and consBatteryVoltage seen in a LOOP packet and puts them in the archive
>>>> record.
>>>>
>>>> Replace bin/weewx/drivers/vantage.py with this version:
>>>>
>>>> wget
>>>> https://sourceforge.net/p/weewx/code/HEAD/tree/trunk/bin/weewx/drivers/vantage.py?format=raw
>>>> -O vantage.py
>>>>
>>>> -tk
>>>>
>>>>
>>>> On Mon, Feb 24, 2014 at 3:00 AM, Andrew Milner <andrew....@gmail.com>
>>>> wrote:
>>>>
>>>>> mesowx would certainly be able to pick it up from the LOOP records and
>>>>> store in its raw table - and display it graphically also (from the raw
>>>>> table).... just not sure how to get the data from the raw table to the
>>>>> archive table!!!  My mesowx raw table contains data from LOOP records that
>>>>> I am 99% sure nobody else is picking up or doing anything with - but I 
>>>>> have
>>>>> no real need to move it from raw to archive as I also extended the 
>>>>> duration
>>>>> of raw to be 14 days!!
>>>>>
>>>>>
>>>>> On 24 February 2014 09:41, Al <vanilla...@gmail.com> wrote:
>>>>>
>>>>>> Andrew Milner wrote:
>>>>>> > Doesn't the stats database have the info?  I don't have a davis
>>>>>> station,
>>>>>> > but looking at your post it seems as though you have changed the
>>>>>> schema
>>>>>> > and restarted weewx - in which case the stats DB should have been
>>>>>> > rebuilt with the additional info - and the stats part of weewx does
>>>>>> > (configurable in weewx.conf) process loop records.... so I would
>>>>>> have
>>>>>> > thought it would 'capture' your info.
>>>>>> > Of course I may be completely off on a tangent here - as I said I
>>>>>> don't
>>>>>> > have a vantage or the luxury of battery statuses!!
>>>>>>
>>>>>> That's not how it's working here. The data is in the loop record, but
>>>>>> apparently consBatteryVoltage and txBatteryStatus data cannot be
>>>>>> captured. So nothing in either the stats DB or the archive DB.
>>>>>>
>>>>>> I'm coming from wview, where consBatteryVoltage and txBatteryStatus
>>>>>> data
>>>>>> is in the archive DB, so I thought that it would be the same with
>>>>>> weewx.
>>>>>>
>>>>>> I have seen at least one Vantage station running weewx that has the
>>>>>> consBatteryVoltage/txBatteryStatus plot icons with current data, so it
>>>>>> must be possible.
>>>>>>
>>>>>> Al
>>>>>>
>>>>>>
>>>>>> --
>>>>>> You received this message because you are subscribed to a topic in
>>>>>> the Google Groups "Weewx user's group" group.
>>>>>> To unsubscribe from this topic, visit
>>>>>> https://groups.google.com/d/topic/weewx-user/q-k9stqZqYQ/unsubscribe.
>>>>>> To unsubscribe from this group and all its topics, send an email to
>>>>>> weewx...@googlegroups.com.
>>>>>>
>>>>>> For more options, visit https://groups.google.com/groups/opt_out.
>>>>>>
>>>>>
>>>>> --
>>>>> You received this message because you are subscribed to the Google
>>>>> Groups "Weewx user's group" group.
>>>>> To unsubscribe from this group and stop receiving emails from it, send
>>>>> an email to weewx...@googlegroups.com.
>>>>> For more options, visit https://groups.google.com/groups/opt_out.
>>>>>
>>>>
>>>> --
>>> 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.
>>> To view this discussion on the web visit
>>> https://groups.google.com/d/msgid/weewx-user/e42336df-3f6f-4884-9339-17dc9af8d59a%40googlegroups.com
>>> <https://groups.google.com/d/msgid/weewx-user/e42336df-3f6f-4884-9339-17dc9af8d59a%40googlegroups.com?utm_medium=email&utm_source=footer>
>>> .
>>>
>> --
>> You received this message because you are subscribed to a topic in the
>> Google Groups "weewx-user" group.
>> To unsubscribe from this topic, visit
>> https://groups.google.com/d/topic/weewx-user/q-k9stqZqYQ/unsubscribe.
>> To unsubscribe from this group and all its topics, send an email to
>> weewx-user+unsubscr...@googlegroups.com.
>> To view this discussion on the web visit
>> https://groups.google.com/d/msgid/weewx-user/CAPq0zEDaNCHwLvXHrj4Rek8prjXv-x9inYeEKy1iNpZyCjdKNQ%40mail.gmail.com
>> <https://groups.google.com/d/msgid/weewx-user/CAPq0zEDaNCHwLvXHrj4Rek8prjXv-x9inYeEKy1iNpZyCjdKNQ%40mail.gmail.com?utm_medium=email&utm_source=footer>
>> .
>>
>> --
>> 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.
>> To view this discussion on the web visit
>> https://groups.google.com/d/msgid/weewx-user/B7BE87B4-3729-4C0E-B008-9864C536420E%40cusick.org.uk
>> <https://groups.google.com/d/msgid/weewx-user/B7BE87B4-3729-4C0E-B008-9864C536420E%40cusick.org.uk?utm_medium=email&utm_source=footer>
>> .
>>
> --
> You received this message because you are subscribed to a topic in the
> Google Groups "weewx-user" group.
> To unsubscribe from this topic, visit
> https://groups.google.com/d/topic/weewx-user/q-k9stqZqYQ/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to
> weewx-user+unsubscr...@googlegroups.com.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/weewx-user/CAPq0zEAA44zLHso4N3bxGRGvy9exPFEf%3D5nUfB2LQ6ze5u_xcQ%40mail.gmail.com
> <https://groups.google.com/d/msgid/weewx-user/CAPq0zEAA44zLHso4N3bxGRGvy9exPFEf%3D5nUfB2LQ6ze5u_xcQ%40mail.gmail.com?utm_medium=email&utm_source=footer>
> .
>
> --
> 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.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/weewx-user/89B7E43F-45CE-46E3-93BE-F16B00933621%40cusick.org.uk
> <https://groups.google.com/d/msgid/weewx-user/89B7E43F-45CE-46E3-93BE-F16B00933621%40cusick.org.uk?utm_medium=email&utm_source=footer>
> .
>

-- 
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.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/weewx-user/CAPq0zEAedUPFjLCFu152Gb2uCbq-uDs_Ctv%2Bp8LXopZQ%2BYAFLw%40mail.gmail.com.
#
#    Copyright (c) 2009-2016 Tom Keffer <tkef...@gmail.com>
#
#    See the file LICENSE.txt for your full rights.
#
"""Statistical accumulators. They accumulate the highs, lows, averages,
etc., of a sequence of records."""

import math
import syslog

import configobj

import weewx

class OutOfSpan(ValueError):
    """Raised when attempting to add a record outside of the timespan held by an accumulator"""

#===============================================================================
#                             ScalarStats
#===============================================================================

class ScalarStats(object):
    """Accumulates statistics (min, max, average, etc.) for a scalar value.
    
    Property 'last' is the last non-None value seen. Property 'lasttime' is
    the time it was seen. """
    
    default_init = (None, None, None, None, 0.0, 0, 0.0, 0)
    
    def __init__(self, stats_tuple=None):
        self.setStats(stats_tuple)
        self.last     = None
        self.lasttime = None
         
    def setStats(self, stats_tuple=None):
        (self.min, self.mintime,
         self.max, self.maxtime,
         self.sum, self.count,
         self.wsum,self.sumtime) = stats_tuple if stats_tuple else ScalarStats.default_init
         
    def getStatsTuple(self):
        """Return a stats-tuple. That is, a tuple containing the gathered statistics.
        This tuple can be used to update the stats database"""
        return (self.min, self.mintime, self.max, self.maxtime, 
                self.sum, self.count, self.wsum, self.sumtime)
    
    def mergeHiLo(self, x_stats):
        """Merge the highs and lows of another accumulator into myself."""
        if x_stats.min is not None:
            if self.min is None or x_stats.min < self.min:
                self.min     = x_stats.min
                self.mintime = x_stats.mintime
        if x_stats.max is not None:
            if self.max is None or x_stats.max > self.max:
                self.max     = x_stats.max
                self.maxtime = x_stats.maxtime
        if x_stats.lasttime is not None:
            if self.lasttime is None or x_stats.lasttime >= self.lasttime:
                self.lasttime = x_stats.lasttime
                self.last     = x_stats.last

    def mergeSum(self, x_stats):
        """Merge the sum and count of another accumulator into myself."""
        self.sum     += x_stats.sum
        self.count   += x_stats.count
        self.wsum    += x_stats.wsum
        self.sumtime += x_stats.sumtime

    def addHiLo(self, val, ts):
        """Include a scalar value in my highs and lows.
        val: A scalar value
        ts:  The timestamp.
        """
        if val is not None:
            # Check for non-numbers and for NaN
            if not isinstance(val, (float, int)) or val != val:
                raise ValueError("accum: ScalarStats.addHiLo expected float or int, "
                                 "got type '%s' ('%s')" % (type(val), val))
            if self.min is None or val < self.min:
                self.min     = val
                self.mintime = ts
            if self.max is None or val > self.max:
                self.max     = val
                self.maxtime = ts
            if self.lasttime is None or ts >= self.lasttime:
                self.last    = val
                self.lasttime= ts

    def addSum(self, val, weight=1):
        """Add a scalar value to my running sum and count."""
        if val is not None:
            # Check for non-numbers and for NaN
            if not isinstance(val, (float, int)) or val != val:
                raise ValueError("accum: ScalarStats.addSum expected float or int, "
                                 "got type '%s' ('%s')" % (type(val), val))
            self.sum     += val
            self.count   += 1
            self.wsum    += val * weight
            self.sumtime += weight
        
    @property
    def avg(self):
        return self.wsum / self.sumtime if self.count else None

class VecStats(object):
    """Accumulates statistics for a vector value.
     
    Property 'last' is the last non-None value seen. It is a two-way tuple (mag, dir).
    Property 'lasttime' is the time it was seen. """

    default_init = (None, None, None, None, 
                    0.0, 0, 0.0, 0, None, 0.0, 0.0, 0, 0.0, 0.0)
     
    def __init__(self, stats_tuple=None):
        self.setStats(stats_tuple)
        self.last     = (None, None)
        self.lasttime = None
 
    def setStats(self, stats_tuple=None):
        (self.min, self.mintime,
         self.max, self.maxtime,
         self.sum, self.count,
         self.wsum,self.sumtime,
         self.max_dir, self.xsum, self.ysum, 
         self.dirsumtime, self.squaresum, 
         self.wsquaresum) = stats_tuple if stats_tuple else VecStats.default_init
        
    def getStatsTuple(self):
        """Return a stats-tuple. That is, a tuple containing the gathered statistics."""
        return (self.min, self.mintime,
                self.max, self.maxtime,
                self.sum, self.count,
                self.wsum,self.sumtime,
                self.max_dir, self.xsum, self.ysum, 
                self.dirsumtime,  self.squaresum, self.wsquaresum)
 
    def mergeHiLo(self, x_stats):
        """Merge the highs and lows of another accumulator into myself."""
        if x_stats.min is not None:
            if self.min is None or x_stats.min < self.min:
                self.min     = x_stats.min
                self.mintime = x_stats.mintime
        if x_stats.max is not None:
            if self.max is None or x_stats.max > self.max:
                self.max     = x_stats.max
                self.maxtime = x_stats.maxtime
                self.max_dir = x_stats.max_dir
        if x_stats.lasttime is not None:
            if self.lasttime is None or x_stats.lasttime >= self.lasttime:
                self.lasttime = x_stats.lasttime
                self.last     = x_stats.last
 
    def mergeSum(self, x_stats):
        """Merge the sum and count of another accumulator into myself."""
        self.sum        += x_stats.sum
        self.count      += x_stats.count
        self.wsum       += x_stats.wsum
        self.sumtime    += x_stats.sumtime
        self.xsum       += x_stats.xsum
        self.ysum       += x_stats.ysum
        self.dirsumtime += x_stats.dirsumtime
        self.squaresum  += x_stats.squaresum
        self.wsquaresum += x_stats.wsquaresum
         
    def addHiLo(self, val, ts):
        """Include a vector value in my highs and lows.
        val: A vector value. It is a 2-way tuple (mag, dir).
        ts:  The timestamp.
        """
        speed, dirN = val
        if speed is not None:
            # Check for non-numbers and for NaN
            if not isinstance(speed, (float, int)) or speed != speed:
                raise ValueError("accum: VecStats.addHiLo expected float or int, "
                                 "got type '%s' ('%s')" % (type(speed), speed))
            if self.min is None or speed < self.min:
                self.min = speed
                self.mintime = ts
            if self.max is None or speed > self.max:
                self.max = speed
                self.maxtime = ts
                self.max_dir = dirN
            if self.lasttime is None or ts >= self.lasttime:
                self.last    = (speed, dirN)
                self.lasttime= ts
         
    def addSum(self, val, weight=1):
        """Add a vector value to my sum and squaresum.
        val: A vector value. It is a 2-way tuple (mag, dir)
        """
        speed, dirN = val
        if speed is not None:
            # Check for non-numbers and for NaN
            if not isinstance(speed, (float, int)) or speed != speed:
                raise ValueError("accum: VecStats.addSum expected float or int, "
                                 "got type '%s' ('%s')" % (type(speed), speed))
            self.sum         += speed
            self.count       += 1
            self.wsum        += weight * speed
            self.sumtime     += weight
            self.squaresum   += speed**2
            self.wsquaresum  += weight * speed**2
            if dirN is not None :
                self.xsum += weight * speed * math.cos(math.radians(90.0 - dirN))
                self.ysum += weight * speed * math.sin(math.radians(90.0 - dirN))
                self.dirsumtime += weight
             
    @property
    def avg(self):
        return self.wsum / self.sumtime if self.count else None
 
    @property
    def rms(self):
        return math.sqrt(self.wsquaresum / self.sumtime) if self.count else None
 
    @property
    def vec_avg(self):
        if self.count:
            return math.sqrt((self.xsum**2 + self.ysum**2) / self.sumtime**2)
 
    @property
    def vec_dir(self):
        if self.dirsumtime and (self.ysum or self.xsum):
            _result = 90.0 - math.degrees(math.atan2(self.ysum, self.xsum))
            if _result < 0.0:
                _result += 360.0
            return _result
        # Return the last known direction when our vector sum is 0
        return self.last[1]

#===============================================================================
#                             Class Accum
#===============================================================================

class Accum(dict):
    """Accumulates statistics for a set of observation types."""
    
    def __init__(self, timespan):
        """Initialize a Accum.
        
        timespan: The time period over which stats will be accumulated."""
        
        self.timespan = timespan
        # The unit system is left unspecified until the first observation comes in.
        self.unit_system = None
        
    def addRecord(self, record, add_hilo=True, weight=1):
        """Add a record to my running statistics. 
        
        The record must have keys 'dateTime' and 'usUnits'."""
        
        # Check to see if the record is within my observation timespan 
        if not self.timespan.includesArchiveTime(record['dateTime']):
            raise OutOfSpan("Attempt to add out-of-interval record")

        for obs_type in record:
            # Get the proper function ...
            func = get_add_function(obs_type)
            # ... then call it.
            func(self, record, obs_type, add_hilo, weight)
                            
    def updateHiLo(self, accumulator):
        """Merge the high/low stats of another accumulator into me."""
        if accumulator.timespan.start < self.timespan.start or accumulator.timespan.stop > self.timespan.stop:
            raise OutOfSpan("Attempt to merge an accumulator whose timespan is not a subset")

        self._check_units(accumulator.unit_system)
        
        for obs_type in accumulator:
            # Initialize the type if we have not seen it before
            self._init_type(obs_type)
            
            # Get the proper function ...
            func = get_merge_function(obs_type)
            # ... then call it
            func(self, accumulator, obs_type)

    def getRecord(self):
        """Extract a record out of the results in the accumulator."""
        
        # All records have a timestamp and unit type
        record = {'dateTime': self.timespan.stop,
                  'usUnits' : self.unit_system}
        
        return self.augmentRecord(record)
    
    def augmentRecord(self, record):

        syslog.syslog(syslog.LOG_INFO, "txBatteryStatus in accum:  %r" % bool('txBatteryStatus' in self))
        syslog.syslog(syslog.LOG_INFO, "txBatteryStatus in record: %r, value %s" % ('txBatteryStatus' in record, record.get('txBatteryStatus')))
        syslog.syslog(syslog.LOG_INFO, "Extract function=%s" % get_extract_function('txBatteryStatus'))
        if 'txBatteryStatus' in self:
            syslog.syslog(syslog.LOG_INFO, "Min:%s; mintime:%s; max:%s; maxtime:%s; sum:%s; count:%s; wsum:%s; sumtime:%s" % self['txBatteryStatus'].getStatsTuple())
            syslog.syslog(syslog.LOG_INFO, "Avg value is %s" % self['txBatteryStatus'].avg)
        # Go through all observation types.
        for obs_type in self:
            # If the type does not appear in the record, then add it:
            if obs_type not in record:
                # Get the proper extraction function...
                func = get_extract_function(obs_type)
                # ... then call it
                func(self, record, obs_type)

        return record

    def set_stats(self, obs_type, stats_tuple):
        
        self._init_type(obs_type)
        self[obs_type].setStats(stats_tuple)

    #
    # Begin add functions. These add a record to the accumulator.
    #
            
    def add_value(self, record, obs_type, add_hilo, weight):
        """Add a single observation to myself."""

        val = record[obs_type]

        # If the type has not been seen before, initialize it
        self._init_type(obs_type)
        # Then add to highs/lows, and to the running sum:
        if add_hilo: 
            self[obs_type].addHiLo(val, record['dateTime'])
        self[obs_type].addSum(val, weight=weight)

    def add_wind_value(self, record, obs_type, add_hilo, weight):
        """Add a single observation of type wind to myself."""

        if obs_type in ['windDir', 'windGust', 'windGustDir']:
            return
        if weewx.debug:
            assert(obs_type == 'windSpeed')
        
        # First add it to regular old 'windSpeed', then
        # treat it like a vector.
        self.add_value(record, obs_type, add_hilo, weight)
        
        # If the type has not been seen before, initialize it.
        self._init_type('wind')
        # Then add to highs/lows.
        if add_hilo:
            self['wind'].addHiLo((record.get('windSpeed'), record.get('windDir')),
                                 record['dateTime'])
            # If the station does not provide windGustDir, then substitute windDir.
            # See issue #320, https://bit.ly/2HSo0ju
            self['wind'].addHiLo((record.get('windGust'), record.get('windGustDir', record.get('windDir'))),
                                 record['dateTime'])
        # Add to the running sum.
        self['wind'].addSum((record['windSpeed'], record.get('windDir')), weight=weight)
        
    def check_units(self, record, obs_type, add_hilo, weight):  # @UnusedVariable
        if weewx.debug:
            assert(obs_type == 'usUnits')
        self._check_units(record['usUnits'])

    def noop(self, record, obs_type, add_hilo=True, weight=1):
        pass

    #
    # Begin hi/lo merge functions. These are called when merging two accumulators
    #
    
    def merge_minmax(self, x_accumulator, obs_type):
        """Merge value in another accumulator, using min/max"""

        self[obs_type].mergeHiLo(x_accumulator[obs_type])

    def merge_avg(self, x_accumulator, obs_type):
        """Merge value in another accumulator, using avg for max"""
        x_stats = x_accumulator[obs_type]
        if x_stats.min is not None:
            if self[obs_type].min is None or x_stats.min < self[obs_type].min:
                self[obs_type].min     = x_stats.min
                self[obs_type].mintime = x_stats.mintime
        if x_stats.avg is not None:
            if self[obs_type].max is None or x_stats.avg > self[obs_type].max:
                self[obs_type].max     = x_stats.avg
                self[obs_type].maxtime = x_accumulator.timespan.stop
        if x_stats.lasttime is not None:
            if self[obs_type].lasttime is None or x_stats.lasttime >= self[obs_type].lasttime:
                self[obs_type].lasttime = x_stats.lasttime
                self[obs_type].last     = x_stats.last

    #
    # Begin extraction functions. These extract a record out of the accumulator.
    #            

    def extract_wind(self, record, obs_type):
        """Extract wind values from myself, and put in a record."""
        # Wind records must be flattened into the separate categories:
        if 'windSpeed' not in record:
            record['windSpeed']   = self[obs_type].avg
        if 'windDir' not in record:
            record['windDir']     = self[obs_type].vec_dir
        if 'windGust' not in record:
            record['windGust']    = self[obs_type].max
        if 'windGustDir' not in record:
            record['windGustDir'] = self[obs_type].max_dir
        
    def extract_sum(self, record, obs_type):
        record[obs_type] = self[obs_type].sum
        
    def extract_last(self, record, obs_type):
        record[obs_type] = self[obs_type].last
        
    def extract_avg(self, record, obs_type):
        record[obs_type] = self[obs_type].avg
        
    def extract_min(self, record, obs_type):
        record[obs_type] = self[obs_type].min
        
    def extract_max(self, record, obs_type):
        record[obs_type] = self[obs_type].max
        
    def extract_count(self, record, obs_type):
        record[obs_type] = self[obs_type].count
        

    #
    # Miscellaneous, utility functions
    #
    
    def _init_type(self, obs_type):
        """Add a given observation type to my dictionary."""
        # Do nothing if this type has already been initialized:
        if obs_type in self:
            return

        # Get a new accumulator of the proper type
        self[obs_type] = new_accumulator(obs_type)

    def _check_units(self, new_unit_system):
        # If no unit system has been specified for me yet, adopt the incoming
        # system
        if self.unit_system is None:
            self.unit_system = new_unit_system
        else:
            # Otherwise, make sure they match
            if self.unit_system != new_unit_system:
                raise ValueError("Unit system mismatch %d v. %d" % (self.unit_system, 
                                                                    new_unit_system))

    @property
    def isEmpty(self):
        return self.unit_system is None
            
#===============================================================================
#                            Configuration dictionaries
#===============================================================================

#
# Mappings from convenient string names, which can be used in a config file,
# to actual functions and classes
#

accum_types = {'scalar' : ScalarStats,
               'vector' : VecStats}

add_functions = {'add'         : Accum.add_value,
                 'add_wind'    : Accum.add_wind_value,
                 'check_units' : Accum.check_units,
                 'noop'        : Accum.noop}

merge_functions = {'minmax' : Accum.merge_minmax,
                   'avg'    : Accum.merge_avg}

extract_functions = {'avg'  : Accum.extract_avg,
                     'sum'  : Accum.extract_sum,
                     'min'  : Accum.extract_min,
                     'max'  : Accum.extract_max,
                     'count': Accum.extract_count,
                     'last' : Accum.extract_last,
                     'wind' : Accum.extract_wind,
                     'noop' : Accum.noop}

#
# Default mappings from observation types to accumulator classes and functions
#

defaults_ini = """
[Accumulator]
    [[dateTime]]
        adder = noop
    [[usUnits]]
        adder = check_units
    [[rain]]
        extractor = sum
    [[ET]]
        extractor = sum
    [[dayET]]
        extractor = last
    [[monthET]]
        extractor = last
    [[yearET]]
        extractor = last
    [[hourRain]]
        extractor = last
    [[dayRain]]
        extractor = last
    [[rain24]]
        extractor = last
    [[monthRain]]
        extractor = last
    [[yearRain]]
        extractor = last
    [[totalRain]]
        extractor = last
    [[stormRain]]
        extractor = last
    [[wind]]
        accumulator = vector
        extractor = wind
    [[windSpeed]]
        adder = add_wind
        merger = avg
        extractor = noop
    [[windDir]]
        extractor = noop
    [[windGust]]
        extractor = noop
    [[windGustDir]]
        extractor = noop
"""
try:
    # Python 2
    from StringIO import StringIO
except ImportError:
    # Python 3
    from io import StringIO
defaults = configobj.ConfigObj(StringIO(defaults_ini))
del StringIO

accum_type_dict = None
add_dict        = None
merge_dict      = None
extract_dict    = None

def initialize(config_dict):
    """Must be called before using any of the accumulators"""
    
    global defaults, accum_type_dict, merge_dict, add_dict, extract_dict

    accum_type_dict = {}
    add_dict        = {}
    merge_dict      = {}
    extract_dict    = {}
    
    # Initialize with the default values:    
    _initialize(defaults)

    # Now do the overrides from the config file
    _initialize(config_dict)
    
def _initialize(config_dict):
    global accum_type_dict, add_dict, merge_dict, extract_dict

    extras = config_dict.get('Accumulator', configobj.ConfigObj({}))
    for obs_type in extras.sections:
        # Get the accumulator type
        accum_type = extras[obs_type].get('accumulator', 'scalar').lower()
        # Fail hard if this is an unknown accumulator type
        accum_type_dict[obs_type] = accum_types[accum_type]
        
        # Get the adder function to use
        add_function = extras[obs_type].get('adder', 'add').lower()
        # Fail hard if this is an unknown adder function
        add_dict[obs_type] = add_functions[add_function]

        # Get the Hi/Lo function to use
        hilo_function = extras[obs_type].get('merger', 'minmax').lower()
        # Fail hard if this is an unknown Hi/Lo function
        merge_dict[obs_type] = merge_functions[hilo_function]
        
        # Get the type of extraction function to use
        extract_function = extras[obs_type].get('extractor', 'avg').lower()
        # Fail hard if this is an unknown extraction type:
        extract_dict[obs_type] = extract_functions[extract_function]
    
def new_accumulator(obs_type):
    global accum_type_dict
    # If the dictionaries have not been initialized, do so with the defaults
    if accum_type_dict is None:
        initialize(defaults)
    return accum_type_dict.get(obs_type, ScalarStats)()

def get_add_function(obs_type):
    global add_dict
    # If the dictionaries have not been initialized, do so with the defaults
    if add_dict is None:
        initialize(defaults)
    return add_dict.get(obs_type, Accum.add_value)
    
def get_merge_function(obs_type):
    global merge_dict
    # If the dictionary has not been initialized, do so with the defaults
    if merge_dict is None:
        initialize(defaults)
    return merge_dict.get(obs_type, Accum.merge_minmax)

def get_extract_function(obs_type):
    global extract_dict
    # If the dictionaries have not been initialized, do so with the defaults
    if extract_dict is None:
        initialize(defaults)
    return extract_dict.get(obs_type, Accum.extract_avg)

Reply via email to