Thanks, Paul

Could you temporarily replace your copy of units.py with the attached? It
will log some clues about what's causing the decode error.



On Wed, Jan 22, 2020 at 6:04 PM P Simmons <mbatra...@gmail.com> wrote:

> Howdy! I had no problems using v4.0.0b6 but decided to upgrade to v4.0.0b9.
>
> I'm now having problems with report generation.
>
> Here are my (sanitized) conf and log files.
>
> Thank you,
> Paul
>
> --
> 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/09a70440-5968-43ba-a1ef-6ee23306f2d2%40googlegroups.com
> <https://groups.google.com/d/msgid/weewx-user/09a70440-5968-43ba-a1ef-6ee23306f2d2%40googlegroups.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/CAPq0zEBh-n0jc%3DqfonvQZz_ziS%2BJDRVMTQeMuBZ-iB1ri8RGzg%40mail.gmail.com.
# -*- coding: utf-8 -*-
#
#    Copyright (c) 2009-2019 Tom Keffer <tkef...@gmail.com>
#
#    See the file LICENSE.txt for your full rights.
#

"""Data structures and functions for dealing with units."""

#
# The unittest examples work only under Python 3!!
#

from __future__ import absolute_import
from __future__ import print_function
import locale
import logging
import time

import weewx
import weeutil.weeutil
from weeutil.weeutil import ListOfDicts

log = logging.getLogger(__name__)

# Handy conversion constants and functions:
INHG_PER_MBAR  = 0.0295299875
MM_PER_INCH    = 25.4
CM_PER_INCH    = MM_PER_INCH / 10.0
METER_PER_MILE = 1609.34
METER_PER_FOOT = METER_PER_MILE / 5280.0
MILE_PER_KM    = 1000.0 / METER_PER_MILE

def CtoK(x):
    return x + 273.15

def CtoF(x):
    return x * 1.8 + 32.0

def FtoC(x):
    return (x - 32.0) * 5.0 / 9.0

# Conversions to and from Felsius. 
# For the definition of Felsius, see https://xkcd.com/1923/
def FtoE(x):
    return (7.0 * x - 80.0) / 9.0

def EtoF(x):
    return (9.0 * x + 80.0) / 7.0

def CtoE(x):
    return (7.0 / 5.0) * x + 16.0
    
def EtoC(x):
    return (x - 16.0) * 5.0 / 7.0

def mps_to_mph(x):
    return x * 3600.0 / METER_PER_MILE

def kph_to_mph(x):
    return x * 1000.0 / METER_PER_MILE

class UnknownType(object):
    """Indicates that the observation type is unknown."""
    def __init__(self, obs_type):
        self.obs_type = obs_type

unit_constants = {
    'US'       : weewx.US,
    'METRIC'   : weewx.METRIC,
    'METRICWX' : weewx.METRICWX
}

unit_nicknames = {
    weewx.US       : 'US',
    weewx.METRIC   : 'METRIC',
    weewx.METRICWX : 'METRICWX'
}

# This data structure maps observation types to a "unit group"
# We start with a standard object group dictionary, but users are
# free to extend it:
obs_group_dict = ListOfDicts({
    "altitude"           : "group_altitude",
    "cloudbase"          : "group_altitude",
    "AqPM2_5"            : "group_concentration",
    "AqPM10"             : "group_concentration",
    "leafWet1"           : "group_count",
    "leafWet2"           : "group_count",
    "cooldeg"            : "group_degree_day",
    "heatdeg"            : "group_degree_day",
    "growdeg"            : "group_degree_day",
    "gustdir"            : "group_direction",
    "vecdir"             : "group_direction",
    "windDir"            : "group_direction",
    "windGustDir"        : "group_direction",
    "windrun"            : "group_distance",
    "interval"           : "group_interval",
    "soilMoist1"         : "group_moisture",
    "soilMoist2"         : "group_moisture",
    "soilMoist3"         : "group_moisture",
    "soilMoist4"         : "group_moisture",
    "extraHumid1"        : "group_percent",
    "extraHumid2"        : "group_percent",
    "extraHumid3"        : "group_percent",
    "extraHumid4"        : "group_percent",
    "extraHumid5"        : "group_percent",
    "extraHumid6"        : "group_percent",
    "extraHumid7"        : "group_percent",
    "inHumidity"         : "group_percent",
    "outHumidity"        : "group_percent",
    "rxCheckPercent"     : "group_percent",
    "altimeter"          : "group_pressure",
    "barometer"          : "group_pressure",
    "pressure"           : "group_pressure",
    "altimeterRate"      : "group_pressurerate",
    "barometerRate"      : "group_pressurerate",
    "pressureRate"       : "group_pressurerate",
    "radiation"          : "group_radiation",
    "maxSolarRad"        : "group_radiation",
    "ET"                 : "group_rain",
    "dayRain"            : "group_rain",
    "hail"               : "group_rain",
    "hourRain"           : "group_rain",
    "monthRain"          : "group_rain",
    "rain"               : "group_rain",
    "snow"               : "group_rain",
    "rain24"             : "group_rain",
    "totalRain"          : "group_rain",
    "stormRain"          : "group_rain",
    "yearRain"           : "group_rain",
    "hailRate"           : "group_rainrate",
    "rainRate"           : "group_rainrate",
    "wind"               : "group_speed",
    "windGust"           : "group_speed",
    "windSpeed"          : "group_speed",
    "windSpeed10"        : "group_speed",
    "windgustvec"        : "group_speed",
    "windvec"            : "group_speed",
    "rms"                : "group_speed2",
    "vecavg"             : "group_speed2",
    "appTemp"            : "group_temperature",
    "dewpoint"           : "group_temperature",
    "inDewpoint"         : "group_temperature",
    "extraTemp1"         : "group_temperature",
    "extraTemp2"         : "group_temperature",
    "extraTemp3"         : "group_temperature",
    "extraTemp4"         : "group_temperature",
    "extraTemp5"         : "group_temperature",
    "extraTemp6"         : "group_temperature",
    "extraTemp7"         : "group_temperature",
    "heatindex"          : "group_temperature",
    "heatingTemp"        : "group_temperature",
    "humidex"            : "group_temperature",
    "inTemp"             : "group_temperature",
    "leafTemp1"          : "group_temperature",
    "leafTemp2"          : "group_temperature",
    "leafTemp3"          : "group_temperature",
    "leafTemp4"          : "group_temperature",
    "outTemp"            : "group_temperature",
    "soilTemp1"          : "group_temperature",
    "soilTemp2"          : "group_temperature",
    "soilTemp3"          : "group_temperature",
    "soilTemp4"          : "group_temperature",
    "windchill"          : "group_temperature",
    "dateTime"           : "group_time",
    "stormStart"         : "group_time",
    "UV"                 : "group_uv",
    "consBatteryVoltage" : "group_volt",
    "heatingVoltage"     : "group_volt",
    "referenceVoltage"   : "group_volt",
    "supplyVoltage"      : "group_volt"
})

# Some aggregations when applied to a type result in a different unit
# group. This data structure maps aggregation type to the group:
agg_group = {
    "firsttime"  : "group_time",
    "lasttime"   : "group_time",
    "maxsumtime" : "group_time",
    "minsumtime" : "group_time",
    'count'      : "group_count",
    'gustdir'    : "group_direction",
    'max_ge'     : "group_count",
    'max_le'     : "group_count",
    'maxmintime' : "group_time",
    'maxtime'    : "group_time",
    'min_ge'     : "group_count",
    'min_le'     : "group_count",
    'minmaxtime' : "group_time",
    'mintime'    : "group_time",
    'sum_ge'     : "group_count",
    'sum_le'     : "group_count",
    'vecdir'     : "group_direction",
}

# This dictionary maps unit groups to a standard unit type in the 
# US customary unit system:
USUnits = ListOfDicts({
    "group_altitude"    : "foot",
    "group_amp"         : "amp",
    "group_concentration": "microgram_per_meter_cubed",
    "group_count"       : "count",
    "group_data"        : "byte",
    "group_degree_day"  : "degree_F_day",
    "group_deltatime"   : "second",
    "group_direction"   : "degree_compass",
    "group_distance"    : "mile",
    "group_elapsed"     : "second",
    "group_energy"      : "watt_hour",
    "group_energy2"     : "watt_second",
    "group_interval"    : "minute",
    "group_length"      : "inch",
    "group_moisture"    : "centibar",
    "group_percent"     : "percent",
    "group_power"       : "watt",
    "group_pressure"    : "inHg",
    "group_pressurerate": "inHg_per_hour",
    "group_radiation"   : "watt_per_meter_squared",
    "group_rain"        : "inch",
    "group_rainrate"    : "inch_per_hour",
    "group_speed"       : "mile_per_hour",
    "group_speed2"      : "mile_per_hour2",
    "group_temperature" : "degree_F",
    "group_time"        : "unix_epoch",
    "group_uv"          : "uv_index",
    "group_volt"        : "volt",
    "group_volume"      : "gallon"
})

# This dictionary maps unit groups to a standard unit type in the 
# metric unit system:
MetricUnits = ListOfDicts({
    "group_altitude"    : "meter",
    "group_amp"         : "amp",
    "group_concentration": "microgram_per_meter_cubed",
    "group_count"       : "count",
    "group_data"        : "byte",
    "group_degree_day"  : "degree_C_day",
    "group_deltatime"   : "second",
    "group_direction"   : "degree_compass",
    "group_distance"    : "km",
    "group_elapsed"     : "second",
    "group_energy"      : "watt_hour",
    "group_energy2"     : "watt_second",
    "group_interval"    : "minute",
    "group_length"      : "cm",
    "group_moisture"    : "centibar",
    "group_percent"     : "percent",
    "group_power"       : "watt",
    "group_pressure"    : "mbar",
    "group_pressurerate": "mbar_per_hour",
    "group_radiation"   : "watt_per_meter_squared",
    "group_rain"        : "cm",
    "group_rainrate"    : "cm_per_hour",
    "group_speed"       : "km_per_hour",
    "group_speed2"      : "km_per_hour2",
    "group_temperature" : "degree_C",
    "group_time"        : "unix_epoch",
    "group_uv"          : "uv_index",
    "group_volt"        : "volt",
    "group_volume"      : "liter"
})

# This dictionary maps unit groups to a standard unit type in the 
# "Metric WX" unit system. It's the same as the "Metric" system,
# except for rain and speed:
MetricWXUnits = ListOfDicts(*MetricUnits.maps)
MetricWXUnits.prepend({
    'group_rain': 'mm',
    'group_rainrate' : 'mm_per_hour',
    'group_speed': 'meter_per_second',
    'group_speed2': 'meter_per_second2',
})

std_groups = {
    weewx.US: USUnits,
    weewx.METRIC: MetricUnits,
    weewx.METRICWX: MetricWXUnits
}

# Conversion functions to go from one unit type to another.
conversionDict = {
    'inHg_per_hour'    : {'mbar_per_hour'    : lambda x : x / INHG_PER_MBAR,
                          'hPa_per_hour'     : lambda x : x / INHG_PER_MBAR,
                          'mmHg_per_hour'    : lambda x : x * 25.4},
    'inHg'             : {'mbar'             : lambda x : x / INHG_PER_MBAR,
                          'hPa'              : lambda x : x / INHG_PER_MBAR,
                          'mmHg'             : lambda x : x * 25.4},
    'degree_E'         : {'degree_C'         : EtoC,
                          'degree_F'         : EtoF},
    'degree_F'         : {'degree_C'         : FtoC,
                          'degree_E'         : FtoE},
    'degree_F_day'     : {'degree_C_day'     : lambda x : x * (5.0/9.0)},
    'mile_per_hour'    : {'km_per_hour'      : lambda x : x * 1.609344,
                          'knot'             : lambda x : x * 0.868976242,
                          'meter_per_second' : lambda x : x * 0.44704},
    'mile_per_hour2'   : {'km_per_hour2'     : lambda x : x * 1.609344,
                          'knot2'            : lambda x : x * 0.868976242,
                          'meter_per_second2': lambda x : x * 0.44704},
    'knot'             : {'mile_per_hour'    : lambda x : x * 1.15077945,
                          'km_per_hour'      : lambda x : x * 1.85200,
                          'meter_per_second' : lambda x : x * 0.514444444},
    'knot2'             : {'mile_per_hour2'  : lambda x : x * 1.15077945,
                           'km_per_hour2'     : lambda x : x * 1.85200,
                           'meter_per_second2': lambda x : x * 0.514444444},
    'inch_per_hour'    : {'cm_per_hour'      : lambda x : x * 2.54,
                          'mm_per_hour'      : lambda x : x * 25.4},
    'inch'             : {'cm'               : lambda x : x * CM_PER_INCH,
                          'mm'               : lambda x : x * MM_PER_INCH},
    'foot'             : {'meter'            : lambda x : x * METER_PER_FOOT},
    'mmHg_per_hour'    : {'inHg_per_hour'    : lambda x : x / MM_PER_INCH,
                          'mbar_per_hour'    : lambda x : x / 0.75006168,
                          'hPa_per_hour'     : lambda x : x / 0.75006168},
    'mmHg'             : {'inHg'             : lambda x : x / MM_PER_INCH,
                          'mbar'             : lambda x : x / 0.75006168,
                          'hPa'              : lambda x : x / 0.75006168},
    'mbar_per_hour'    : {'inHg_per_hour'    : lambda x : x * INHG_PER_MBAR,
                          'mmHg_per_hour'    : lambda x : x * 0.75006168,
                          'hPa_per_hour'     : lambda x : x * 1.0},
    'mbar'             : {'inHg'             : lambda x : x * INHG_PER_MBAR,
                          'mmHg'             : lambda x : x * 0.75006168,
                          'hPa'              : lambda x : x * 1.0},
    'hPa_per_hour'     : {'inHg_per_hour'    : lambda x : x * INHG_PER_MBAR,
                          'mmHg_per_hour'    : lambda x : x * 0.75006168,
                          'mbar_per_hour'    : lambda x : x * 1.0},
    'hPa'              : {'inHg'             : lambda x : x * INHG_PER_MBAR,
                          'mmHg'             : lambda x : x * 0.75006168,
                          'mbar'             : lambda x : x * 1.0},
    'degree_C'         : {'degree_F'         : CtoF,
                          'degree_E'         : CtoE},
    'degree_C_day'     : {'degree_F_day'     : lambda x : x * (9.0/5.0)},
    'km_per_hour'      : {'mile_per_hour'    : kph_to_mph,
                          'knot'             : lambda x : x * 0.539956803,
                          'meter_per_second' : lambda x : x * 0.277777778},
    'meter_per_second' : {'mile_per_hour'    : mps_to_mph,
                          'knot'             : lambda x : x * 1.94384449,
                          'km_per_hour'      : lambda x : x * 3.6},
    'meter_per_second2': {'mile_per_hour2'   : lambda x : x * 2.23693629,
                          'knot2'            : lambda x : x * 1.94384449,
                          'km_per_hour2'     : lambda x : x * 3.6},
    'cm_per_hour'      : {'inch_per_hour'    : lambda x : x * 0.393700787,
                          'mm_per_hour'      : lambda x : x * 10.0},
    'mm_per_hour'      : {'inch_per_hour'    : lambda x : x * .0393700787,
                          'cm_per_hour'      : lambda x : x * 0.10},
    'cm'               : {'inch'             : lambda x : x / CM_PER_INCH,
                          'mm'               : lambda x : x * 10.0},
    'mm'               : {'inch'             : lambda x : x / MM_PER_INCH,
                          'cm'               : lambda x : x * 0.10},
    'meter'            : {'foot'             : lambda x : x / METER_PER_FOOT,
                          'km'               : lambda x : x / 1000.0},
    'dublin_jd'        : {'unix_epoch'       : lambda x : (x-25567.5) * 86400.0},
    'unix_epoch'       : {'dublin_jd'        : lambda x : x/86400.0 + 25567.5},
    'second'           : {'hour'             : lambda x : x/3600.0,
                          'minute'           : lambda x : x/60.0,
                          'day'              : lambda x : x/86400.0},
    'minute'           : {'second'           : lambda x : x*60.0,
                          'hour'             : lambda x : x/60.0,
                          'day'              : lambda x : x/1440.0},
    'hour'             : {'second'           : lambda x : x*3600.0,
                          'minute'           : lambda x : x*60.0,
                          'day'              : lambda x : x/24.0},
    'day'              : {'second'           : lambda x : x*86400.0,
                          'minute'           : lambda x : x*1440.0,
                          'hour'             : lambda x : x*24.0},
    'gallon'           : {'liter'            : lambda x : x * 3.78541,
                          'litre'            : lambda x : x * 3.78541,
                          'cubic_foot'       : lambda x : x * 0.133681},
    'liter'            : {'gallon'           : lambda x : x * 0.264172,
                          'cubic_foot'       : lambda x : x * 0.0353147},
    'cubic_foot'       : {'gallon'           : lambda x : x * 7.48052,
                          'litre'            : lambda x : x * 28.3168,
                          'liter'            : lambda x : x * 28.3168},
    'bit'              : {'byte'             : lambda x : x / 8},
    'byte'             : {'bit'              : lambda x : x * 8},
    'km'               : {'meter'            : lambda x : x * 1000.0,
                          'mile'             : lambda x : x * 0.621371192},
    'mile'             : {'km'               : lambda x : x * 1.609344},
    'watt'             : {'kilowatt'         : lambda x : x / 1000.0},
    'kilowatt'         : {'watt'             : lambda x : x * 1000.0},
    'watt_second'      : {'kilowatt_hour'    : lambda x : x / 3.6e6,
                          'watt_hour'        : lambda x : x / 3600.0},
    'watt_hour'        : {'kilowatt_hour'    : lambda x : x / 1000.0,
                          'watt_second'      : lambda x : x * 3600.0},
    'kilowatt_hour'    : {'watt_second'      : lambda x : x * 3.6e6,
                          'watt_hour'        : lambda x : x * 1000.0},
}

# Default unit formatting when nothing specified in skin configuration file
default_unit_format_dict = {
    "amp"                : "%.1f",
    "bit"                : "%.0f",
    "byte"               : "%.0f",
    "centibar"           : "%.0f",
    "cm"                 : "%.2f",
    "cm_per_hour"        : "%.2f",
    "cubic_foot"         : "%.1f",
    "day"                : "%.1f",
    "degree_C"           : "%.1f",
    "degree_C_day"       : "%.1f",
    "degree_E"           : "%.1f",
    "degree_F"           : "%.1f",
    "degree_F_day"       : "%.1f",
    "degree_compass"     : "%.0f",
    "foot"               : "%.0f",
    "gallon"             : "%.1f",
    "hPa"                : "%.1f",
    "hPa_per_hour"       : "%.3f",
    "hour"               : "%.1f",
    "inHg"               : "%.3f",
    "inHg_per_hour"      : "%.5f",
    "inch"               : "%.2f",
    "inch_per_hour"      : "%.2f",
    "kilowatt_hour"      : "%.1f",
    "km"                 : "%.1f",
    "km_per_hour"        : "%.0f",
    "km_per_hour2"       : "%.1f",
    "knot"               : "%.0f",
    "knot2"              : "%.1f",
    "liter"              : "%.1f",
    "litre"              : "%.1f",
    "mbar"               : "%.1f",
    "mbar_per_hour"      : "%.4f",
    "meter"              : "%.0f",
    "meter_per_second"   : "%.0f",
    "meter_per_second2"  : "%.1f",
    "microgram_per_meter_cubed": "%.3f",
    "mile"               : "%.1f",
    "mile_per_hour"      : "%.0f",
    "mile_per_hour2"     : "%.1f",
    "mm"                 : "%.1f",
    "mmHg"               : "%.1f",
    "mmHg_per_hour"      : "%.4f",
    "mm_per_hour"        : "%.1f",
    "percent"            : "%.0f",
    "second"             : "%.0f",
    "uv_index"           : "%.1f",
    "volt"               : "%.1f",
    "watt"               : "%.1f",
    "watt_second"        : "%.0f",
    "watt_hour"          : "%.1f",
    "watt_per_meter_squared" : "%.0f",
    "NONE"              : u"   N/A"
}

# Default unit labels to be used in the absence of a skin configuration file
default_unit_label_dict = {
    "amp"               : u" amp",
    "bit"               : u" b",
    "byte"              : u" B",
    "centibar"          : u" cb",
    "cm"                : u" cm",
    "cm_per_hour"       : u" cm/h",
    "cubic_foot"        : u" ft³",
    "day"               : (u" day", u" days"),
    "degree_C"          : u"°C",
    "degree_C_day"      : u"°C-day",
    "degree_E"          : u"°E",
    "degree_F"          : u"°F",
    "degree_F_day"      : u"°F-day",
    "degree_compass"    : u"°",
    "foot"              : u" feet",
    "gallon"            : u" gal",
    "hPa"               : u" hPa",
    "hPa_per_hour"      : u" hPa/h",
    "inHg"              : u" inHg",
    "inHg_per_hour"     : u" inHg/h",
    "hour"              : (u" hour", u" hours"),
    "inch"              : u" in",
    "inch_per_hour"     : u" in/h",
    "kilowatt_hour"     : u" kWh",
    "km"                : u" km",
    "km_per_hour"       : u" kph",
    "km_per_hour2"      : u" kph",
    "knot"              : u" knots",
    "knot2"             : u" knots",
    "liter"             : u" l",
    "litre"             : u" l",
    "mbar"              : u" mbar",
    "mbar_per_hour"     : u" mbar/h",
    "meter"             : u" meters",
    "meter_per_second"  : u" m/s",
    "meter_per_second2" : u" m/s",
    "microgram_per_meter_cubed": u"µg/m³",
    "mile"              : u" mile",
    "mile_per_hour"     : u" mph",
    "mile_per_hour2"    : u" mph",
    "minute"            : (u" minute", u" minutes"),
    "mm"                : u" mm",
    "mmHg"              : u" mmHg",
    "mm_per_hour"       : u" mm/h",
    "mmHg_per_hour"     : u" mmHg/h",
    "percent"           : u"%",
    "second"            : (u" second", u" seconds"),
    "uv_index"          : u"",
    "volt"              : u" V",
    "watt"              : u" W",
    "watt_second"       : u" Ws",
    "watt_hour"         : u" Wh",
    "watt_per_meter_squared" : u" W/m²",
    "NONE"              : u""
}

# Default strftime formatting to be used in the absence of a skin
# configuration file. The entry for delta_time uses a special
# encoding.
default_time_format_dict = {
    "day"        : "%H:%M",
    "week"       : "%H:%M on %A",
    "month"      : "%d-%b-%Y %H:%M",
    "year"       : "%d-%b-%Y %H:%M",
    "rainyear"   : "%d-%b-%Y %H:%M",
    "current"    : "%d-%b-%Y %H:%M",
    "ephem_day"  : "%H:%M",
    "ephem_year" : "%d-%b-%Y %H:%M",
    "delta_time" : "%(day)d%(day_label)s, %(hour)d%(hour_label)s, "
                   "%(minute)d%(minute_label)s"
}

# Default mapping from compass degrees to ordinals
default_ordinate_names = [
    'N', 'NNE','NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
    'S', 'SSW','SW', 'WSW', 'W', 'WNW', 'NW', 'NNW',
    'N/A'
]

#==============================================================================
#                        class ValueTuple
#==============================================================================

# A value, along with the unit it is in, can be represented by a 3-way tuple
# called a value tuple. All weewx routines can accept a simple unadorned
# 3-way tuple as a value tuple, but they return the type ValueTuple. It is
# useful because its contents can be accessed using named attributes.
#
# Item   attribute   Meaning
#    0    value      The datum value (eg, 20.2)
#    1    unit       The unit it is in ("degree_C")
#    2    group      The unit group ("group_temperature")
#
# It is valid to have a datum value of None.
#
# It is also valid to have a unit type of None (meaning there is no information
# about the unit the value is in). In this case, you won't be able to convert
# it to another unit.

class ValueTuple(tuple):
    def __new__(cls, *args):
        return tuple.__new__(cls, args)
    @property
    def value(self):
        return self[0]
    @property
    def unit(self):
        return self[1]
    @property
    def group(self):
        return self[2]
    # ValueTuples have some modest math abilities: subtraction and addition.
    def __sub__(self, other):
        if self[1] != other[1] or self[2] != other[2]:
            raise TypeError("unsupported operand error for subtraction: %s and %s" % (self[1], other[1]))
        return ValueTuple(self[0] - other[0], self[1], self[2])
    def __add__(self, other):
        if self[1] != other[1] or self[2] != other[2]:
            raise TypeError("unsupported operand error for addition: %s and %s" % (self[1], other[1]))
        return ValueTuple(self[0] + other[0], self[1], self[2])

#==============================================================================
#                        class Formatter
#==============================================================================

class Formatter(object):
    """Holds formatting information for the various unit types.
    
    Examples (using the default formatters):
    >>> import os
    >>> os.environ['TZ'] = 'America/Los_Angeles'
    >>> time.tzset()
    >>> f = Formatter()
    >>> print(f.toString((20.0, "degree_C", "group_temperature")))
    20.0°C
    >>> print(f.toString((83.2, "degree_F", "group_temperature")))
    83.2°F
    >>> # Try the Spanish locale, which will use comma decimal separators.
    >>> # For this to work, the Spanish locale must have been installed.
    >>> # You can do this with the command:
    >>> #     sudo locale-gen es_ES.UTF-8 && sudo update-locale
    >>> x = locale.setlocale(locale.LC_NUMERIC, 'es_ES.utf-8')
    >>> print(f.toString((83.2, "degree_F", "group_temperature"), localize=True))
    83,2°F
    >>> # Try it again, but overriding the localization:
    >>> print(f.toString((83.2, "degree_F", "group_temperature"), localize=False))
    83.2°F
    >>> # Set locale back to default
    >>> x = locale.setlocale(locale.LC_NUMERIC, '')
    >>> print(f.toString((123456789,  "unix_epoch", "group_time")))
    29-Nov-1973 13:33
    >>> print(f.to_ordinal_compass((5.0, "degree_compass", "group_direction")))
    N
    >>> print(f.to_ordinal_compass((0.0, "degree_compass", "group_direction")))
    N
    >>> print(f.to_ordinal_compass((12.5, "degree_compass", "group_direction")))
    NNE
    >>> print(f.to_ordinal_compass((360.0, "degree_compass", "group_direction")))
    N
    >>> print(f.to_ordinal_compass((None, "degree_compass", "group_direction")))
    N/A
    >>> print(f.toString((1*86400 + 1*3600 + 16*60 + 42, "second", "group_deltatime")))
    1 day, 1 hour, 16 minutes
    >>> delta_format = "%(day)d%(day_label)s, %(hour)d%(hour_label)s, %(minute)d%(minute_label)s, %(second)d%(second_label)s" 
    >>> print(f.toString((2*86400 + 3*3600 +  5*60 +  2, "second", "group_deltatime"), useThisFormat=delta_format))
    2 days, 3 hours, 5 minutes, 2 seconds
    """

    def __init__(self, unit_format_dict = default_unit_format_dict,
                       unit_label_dict  = default_unit_label_dict,
                       time_format_dict = default_time_format_dict,
                       ordinate_names   = default_ordinate_names):
        """
        unit_format_dict: Key is unit type (eg, 'inHg'), value is a
        string format ("%.1f")
        
        unit_label_dict: Key is unit type (eg, 'inHg'), value is a
        label (" inHg")
        
        time_format_dict: Key is a context (eg, 'week'), value is a
        strftime format ("%d-%b-%Y %H:%M")."""

        self.unit_format_dict = unit_format_dict
        self.unit_label_dict  = unit_label_dict
        # Make a copy of the time format dictionary. This will stop the
        # unwanted interpolation of key delta_time.
        self.time_format_dict = dict(time_format_dict)
        self.ordinate_names    = ordinate_names
        # Add new keys for backwards compatibility on old skin dictionaries:
        self.time_format_dict.setdefault('ephem_day', "%H:%M")
        self.time_format_dict.setdefault('ephem_year', "%d-%b-%Y %H:%M")
        
    @staticmethod
    def fromSkinDict(skin_dict):
        """Factory static method to initialize from a skin dictionary."""
        try:
            unit_format_dict = skin_dict['Units']['StringFormats']
        except KeyError:
            unit_format_dict = default_unit_format_dict

        try:
            unit_label_dict = skin_dict['Units']['Labels']
        except KeyError:
            unit_label_dict = default_unit_label_dict

        try:
            time_format_dict = skin_dict['Units']['TimeFormats']
        except KeyError:
            time_format_dict = default_time_format_dict

        try:
            ordinate_names = weeutil.weeutil.option_as_list(skin_dict['Units']['Ordinates']['directions'])
        except KeyError:
            ordinate_names = default_ordinate_names

        return Formatter(unit_format_dict,
                         unit_label_dict,
                         time_format_dict,
                         ordinate_names)

    def get_format_string(self, unit):
        """Return a suitable format string."""

        # First, try my internal format dict
        if unit in self.unit_format_dict:
            return self.unit_format_dict[unit]
        # If that didn't work, try the default dict:
        elif unit in default_unit_format_dict:
            return default_unit_format_dict[unit]
        else:
            # Can't find one. Return a generic formatter:
            return '%f'

    def get_label_string(self, unit, plural=True):
        """Return a suitable label.
        
        This function looks up a suitable label in the unit_label_dict. If the
        associated value is a string, it returns it. If it is a tuple or a list,
        then it is assumed the first value is a singular version of the label
        (e.g., "foot"), the second a plural version ("feet"). If the parameter
        plural=False, then the singular version is returned. Otherwise, the
        plural version.
        """

        # First, try my internal label dictionary:
        if unit in self.unit_label_dict:
            label = self.unit_label_dict[unit]
        # If that didn't work, try the default label dictionary:
        elif unit in default_unit_label_dict:
            label = default_unit_label_dict[unit]
        else:
            # Can't find a label. Just return an empty string:
            return u''

        # Is the label a tuple or list?
        if isinstance(label, (tuple, list)):
            # Yes. Return the singular or plural version as requested
            return label[1] if plural else label[0]
        else:
            # No singular/plural version. It's just a string. Return it.
            return label

    def toString(self, val_t, context='current', addLabel=True, 
                 useThisFormat=None, None_string=None, 
                 localize=True):
        """Format the value as a string.
        
        val_t: The value to be formatted as a value tuple. 
        
        context: A time context (eg, 'day'). 
        [Optional. If not given, context 'current' will be used.]
        
        addLabel: True to add a unit label (eg, 'mbar'), False to not.
        [Optional. If not given, a label will be added.]
        
        useThisFormat: An optional string or strftime format to be used. 
        [Optional. If not given, the format given in the initializer will
        be used.]
        
        None_string: A string to be used if the value val is None.
        [Optional. If not given, the string given unit_format_dict['NONE']
        will be used.]
        
        localize: True to localize the results. False otherwise
        """
        if val_t is None or val_t[0] is None:
            if None_string is not None: 
                return None_string
            else:
                return self.unit_format_dict.get('NONE', 'N/A')
            
        if val_t[1] == "unix_epoch":
            # Different formatting routines are used if the value is a time.
            if useThisFormat is not None:
                val_str = time.strftime(useThisFormat, time.localtime(val_t[0]))
            else:
                val_str = time.strftime(self.time_format_dict.get(context, "%d-%b-%Y %H:%M"), time.localtime(val_t[0]))
        elif val_t[2] == "group_deltatime":
            # Get a delta-time format string. Use a default if the user did not supply one:
            if useThisFormat is not None:
                format_string = useThisFormat
            else:
                format_string = self.time_format_dict.get("delta_time", default_time_format_dict["delta_time"])
            # Now format the delta time, using the function delta_secs_to_string:
            val_str = self.delta_secs_to_string(val_t[0], format_string)
            # Return it right away, because it does not take a label
            return val_str
        else:
            # It's not a time. It's a regular value. Get a suitable
            # format string:
            if useThisFormat is None:
                # No user-specified format string. Go get one:
                format_string = self.get_format_string(val_t[1])
            else:
                # User has specified a string. Use it.
                format_string = useThisFormat
            if localize:
                # Localization requested. Use locale with the supplied format:
                val_str = locale.format_string(format_string, val_t[0])
            else:
                # No localization. Just format the string.
                val_str = format_string % val_t[0]

        # Add a label, if requested:
        if addLabel:
            s = self.get_label_string(val_t[1], plural=(not val_t[0]==1))
            try:
                val_str += s
            except UnicodeDecodeError as e:
                log.error("%s", e)
                log.error("val_str=%s; type(val_str)=%s; s=%s; type(s)=%s" % (val_str, type(val_str), s, type(s)))
                raise

        return val_str

    def to_ordinal_compass(self, val_t):
        if val_t[0] is None:
            return self.ordinate_names[-1]
        _sector_size = 360.0 / (len(self.ordinate_names)-1)
        _degree = (val_t[0] + _sector_size/2.0) % 360.0
        _sector = int(_degree / _sector_size)
        return self.ordinate_names[_sector]
    
    def delta_secs_to_string(self, secs, label_format):
        """Convert elapsed seconds to a string
        
        Example:
        >>> f = Formatter()
        >>> print(f.delta_secs_to_string(3*86400+21*3600+7*60+11, default_time_format_dict["delta_time"]))
        3 days, 21 hours, 7 minutes
        """
        etime_dict = {}
        for (label, interval) in (('day', 86400), ('hour', 3600), ('minute', 60), ('second', 1)):
            amt = int(secs // interval)
            etime_dict[label] = amt
            etime_dict[label + '_label'] = self.get_label_string(label, not amt == 1)
            secs %= interval
        # The version of locale in Python version 2.5 and 2.6 cannot handle interpolation and raises an
        # exception. Be prepared to catch it and use a regular % formatter
        try:
            ans = locale.format_string(label_format, etime_dict)
        except TypeError:
            ans = label_format % etime_dict
        return ans

#==============================================================================
#                        class Converter
#==============================================================================

class Converter(object):
    """Holds everything necessary to do conversions to a target unit system."""
    
    def __init__(self, group_unit_dict=USUnits):
        """Initialize an instance of Converter
        
        group_unit_dict: A dictionary holding the conversion information. 
        Key is a unit_group (eg, 'group_pressure'), value is the target
        unit type ('mbar')"""

        self.group_unit_dict  = group_unit_dict
        
    @staticmethod
    def fromSkinDict(skin_dict):
        """Factory static method to initialize from a skin dictionary."""
        try:
            group_unit_dict = skin_dict['Units']['Groups']
        except KeyError:
            group_unit_dict = USUnits
        return Converter(group_unit_dict)

    def convert(self, val_t):
        """Convert a value from a given unit type to the target type.
        
        val_t: A value tuple with the datum, a unit type, and a unit group
        
        returns: A value tuple in the new, target unit type. If the input
        value tuple contains an unknown unit type an exception of type KeyError
        will be thrown. If the input value tuple has either a unit
        type of None, or a group type of None (but not both), then an
        exception of type KeyError will be thrown. If both the
        unit and group are None, then the original val_t will be
        returned (i.e., no conversion is done).
        
        Examples:
        >>> p_m = (1016.5, 'mbar', 'group_pressure')
        >>> c = Converter()
        >>> print("%.3f %s %s" % c.convert(p_m))
        30.017 inHg group_pressure
        
        Try an unspecified unit type:
        >>> p2 = (1016.5, None, None)
        >>> print(c.convert(p2))
        (1016.5, None, None)
        
        Try a bad unit type:
        >>> p3 = (1016.5, 'foo', 'group_pressure')
        >>> try:
        ...     print(c.convert(p3))
        ... except KeyError:
        ...     print("Exception thrown")
        Exception thrown
        
        Try a bad group type:
        >>> p4 = (1016.5, 'mbar', 'group_foo')
        >>> try:
        ...     print(c.convert(p4))
        ... except KeyError:
        ...     print("Exception thrown")
        Exception thrown
        """
        if val_t[1] is None and val_t[2] is None:
            return val_t
        # Determine which units (eg, "mbar") this group should be in.
        # If the user has not specified anything, then fall back to US Units.
        new_unit_type = self.group_unit_dict.get(val_t[2], USUnits[val_t[2]])
        # Now convert to this new unit type: 
        new_val_t = convert(val_t, new_unit_type)
        return new_val_t

    def convertDict(self, obs_dict):
        """Convert an observation dictionary into the target unit system.
        
        The source dictionary must include the key 'usUnits' in order for the
        converter to figure out what unit system it is in.
        
        The output dictionary will contain no information about the unit
        system (that is, it will not contain a 'usUnits' entry). This is
        because the conversion is general: it may not result in a standard
        unit system.
        
        Example: convert a dictionary which is in the metric unit system
        into US units
        
        >>> # Construct a default converter, which will be to US units
        >>> c = Converter()
        >>> # Source dictionary is in metric units
        >>> source_dict = {'dateTime': 194758100, 'outTemp': 20.0,\
            'usUnits': weewx.METRIC, 'barometer':1015.9166, 'interval':15}
        >>> target_dict = c.convertDict(source_dict)
        >>> print("dateTime: %d, interval: %d, barometer: %.3f, outTemp: %.3f" %\
        (target_dict['dateTime'], target_dict['interval'], \
         target_dict['barometer'], target_dict['outTemp']))
        dateTime: 194758100, interval: 15, barometer: 30.000, outTemp: 68.000
        """
        target_dict = {}
        for obs_type in obs_dict:
            if obs_type == 'usUnits': continue
            # Do the conversion, but keep only the first value in
            # the ValueTuple:
            target_dict[obs_type] = self.convert(as_value_tuple(obs_dict, obs_type))[0]
        return target_dict
            
            
    def getTargetUnit(self, obs_type, agg_type=None):
        """Given an observation type and an aggregation type, return the 
        target unit type and group, or (None, None) if they cannot be determined.
        
        obs_type: An observation type ('outTemp', 'rain', etc.)
        
        agg_type: Type of aggregation ('mintime', 'count', etc.)
        [Optional. default is no aggregation)
        
        returns: A 2-way tuple holding the unit type and the unit group
        or (None, None) if they cannot be determined.
        """        
        unit_group = _getUnitGroup(obs_type, agg_type)
        if unit_group in self.group_unit_dict:
            unit_type = self.group_unit_dict[unit_group]
        else:
            unit_type = USUnits.get(unit_group)
        return (unit_type, unit_group)

#==============================================================================
#                         Standard Converters
#==============================================================================

# This dictionary holds converters for the standard unit conversion systems. 
StdUnitConverters = {weewx.US       : Converter(USUnits),
                     weewx.METRIC   : Converter(MetricUnits),
                     weewx.METRICWX : Converter(MetricWXUnits)}

#==============================================================================
#                        class FixedConverter
#==============================================================================

class FixedConverter(object):
    """Dirt simple converter that can only convert to a specified unit."""
    def __init__(self, target_units):
        """Initialize an instance of FixedConverter
        
        target_units: The new, target unit (eg, "degree_C")"""
        self.target_units = target_units
        
    def convert(self, val_t):
        return convert(val_t, self.target_units)
    
#==============================================================================
#                      class ValueHelper
#==============================================================================

class ValueHelper(object):
    """A helper class that binds a value tuple together with everything needed to do a
    context sensitive formatting
    
    Example:
    
    >>> value_t = (68.01, "degree_F", "group_temperature")
    >>> # Use the default converter and formatter:
    >>> vh = ValueHelper(value_t)
    >>> print(vh)
    68.0°F
    
    Try explicit unit conversion:
    >>> print(vh.degree_C)
    20.0°C
    
    Do it again, but using a converter:
    >>> vh = ValueHelper(value_t, converter=Converter(MetricUnits))
    >>> print(vh)
    20.0°C
    
    Extract just the raw value:
    >>> print("%.1f" % vh.raw)
    20.0
    """
    def __init__(self, value_t, context='current', formatter=Formatter(), converter=Converter()):
        """Initialize a ValueHelper.
        
        value_t: A value tuple holding the datum.
        
        context: The time context. Something like 'current', 'day', 'week'.
        [Optional. If not given, context 'current' will be used.]
        
        formatter: An instance of class Formatter.
        [Optional. If not given, then the default Formatter() will be used]
        
        converter: An instance of class Converter.
        [Optional. If not given, then the default Converter() will be used,
        which will convert to US units]
        """
        self.value_t   = value_t
        self.context   = context
        self.formatter = formatter
        self.converter = converter

    def toString(self,
                 addLabel=True,
                 useThisFormat=None,
                 None_string=None,
                 localize=True,
                 NONE_string=None):
        """Convert my internally held ValueTuple to a string, using the supplied
        converter and formatter.

        Parameters:
            addLabel: If True, add a unit label

            useThisFormat: String with a format to be used when formatting the value. If None,
            then a format will be supplied. Default is None.

            None_string: If the value is None, then this string will be used. If None, then a default string
            from skin.conf will be used. Default is None.

            localize: If True, localize the results. Default is True

            NONE_string: Supplied for backwards compatibility. Identical semantics to None_string.
        """
        # If the type is unknown, then just return an error string: 
        if isinstance(self.value_t, UnknownType):
            return "?'%s'?" % self.value_t.obs_type
        # Check NONE_string for backwards compatibility:
        if None_string is None and NONE_string is not None:
            None_string = NONE_string
        # Get the value tuple in the target units:
        vtx = self._raw_value_tuple
        # Then do the format conversion:
        s = self.formatter.toString(vtx, self.context, addLabel=addLabel, 
                                    useThisFormat=useThisFormat, None_string=None_string, 
                                    localize=localize)
        return s
        
    def __str__(self):
        """Return as string"""
        return self.toString()

    def format(self, format_string=None, None_string=None, add_label=True, localize=True):
        """Returns a formatted version of the datum, using user-supplied customizations."""
        return self.toString(useThisFormat=format_string, None_string=None_string,
                             addLabel=add_label, localize=localize)

    def ordinal_compass(self):
        """Returns an ordinal compass direction (eg, 'NNW')"""
        # Get the raw value tuple, then ask the formatter to look up an
        # appropriate ordinate:
        return self.formatter.to_ordinal_compass(self._raw_value_tuple)
        
    @property
    def raw(self):
        """Returns the raw value without any formatting."""
        return self._raw_value_tuple[0]

    # Backwards compatibility
    def string(self, None_string=None):
        """Return as string with an optional user specified string to be
        used if None"""
        return self.toString(None_string=None_string)

    # Backwards compatibility
    def nolabel(self, format_string, None_string=None):
        """Returns a formatted version of the datum, using a user-supplied
        format. No label."""
        return self.toString(addLabel=False, useThisFormat=format_string, None_string=None_string)

    # Backwards compatibility
    @property
    def formatted(self):
        """Return a formatted version of the datum. No label."""
        return self.toString(addLabel=False)

    @property
    def _raw_value_tuple(self):
        """Return a value tuple in the target units."""
        # ... Do the unit conversion ...
        vtx = self.converter.convert(self.value_t)
        # ... and then return it
        return vtx

    def __getattr__(self, target_unit):
        """Convert to a new unit type.
        
        target_unit: The new target unit. 
        
        returns: A ValueHelper with a FixedConverter that converts to the
        specified units."""

        # This is to get around bugs in the Python version of Cheetah's namemapper:
        if target_unit in ['__call__', 'has_key']:
            raise AttributeError

        # If we are being asked to perform a conversion, make sure it's a
        # legal one:
        if self.value_t[1] != target_unit:        
            try:
                conversionDict[self.value_t[1]][target_unit]
            except KeyError:
                raise AttributeError("Illegal conversion from '%s' to '%s'"%(self.value_t[1], target_unit))
        return ValueHelper(self.value_t, self.context, self.formatter, FixedConverter(target_unit))
    
    def exists(self):
        return not isinstance(self.value_t, UnknownType)
    
    def has_data(self):
        return self.exists() and self.value_t[0] is not None
    

#==============================================================================
#                       class UnitInfoHelper and friends
#==============================================================================

class UnitHelper(object):
    def __init__(self, converter):
        self.converter = converter
    def __getattr__(self, obs_type):
        # This is to get around bugs in the Python version of Cheetah's namemapper:
        if obs_type in ['__call__', 'has_key']:
            raise AttributeError
        return self.converter.getTargetUnit(obs_type)[0]

class FormatHelper(object):
    def __init__(self, formatter, converter):
        self.formatter = formatter
        self.converter = converter
    def __getattr__(self, obs_type):
        # This is to get around bugs in the Python version of Cheetah's namemapper:
        if obs_type in ['__call__', 'has_key']:
            raise AttributeError
        return get_format_string(self.formatter, self.converter, obs_type)
    
class LabelHelper(object):
    def __init__(self, formatter, converter):
        self.formatter = formatter
        self.converter = converter
    def __getattr__(self, obs_type):
        # This is to get around bugs in the Python version of Cheetah's namemapper:
        if obs_type in ['__call__', 'has_key']:
            raise AttributeError
        return get_label_string(self.formatter, self.converter, obs_type)
    
class UnitInfoHelper(object):
    """Helper class used for for the $unit template tag."""
    def __init__(self, formatter, converter):
        """
        formatter: an instance of Formatter
        converter: an instance of Converter
        """
        self.unit_type = UnitHelper(converter)
        self.format    = FormatHelper(formatter, converter)
        self.label     = LabelHelper(formatter, converter)
        self.group_unit_dict = converter.group_unit_dict

    # This is here for backwards compatibility:
    @property
    def unit_type_dict(self):
        return self.group_unit_dict


class ObsInfoHelper(object):
    """Helper class to implement the $obs template tag."""    
    def __init__(self, skin_dict):
        try:
            d = skin_dict['Labels']['Generic']
        except KeyError:
            d = {}
        self.label = weeutil.weeutil.KeyDict(d)

#==============================================================================
#                             Helper functions
#==============================================================================
def _getUnitGroup(obs_type, agg_type=None):
    """Given an observation type and an aggregation type, what unit group
    does it belong to?

        Examples:
             obs_type  agg_type          Returns
             ________  ________         _________
            'outTemp',  None      -->  'group_temperature'
            'outTemp', 'min'      -->  'group_temperature'
            'outTemp', 'mintime'  -->  'group_time'
            'wind',    'avg'      -->  'group_speed'
            'wind',    'vecdir'   -->  'group_direction'
        
        obs_type: An observation type. E.g., 'barometer'.
        
        agg_type: An aggregation type E.g., 'mintime', or 'avg'.
        
        Returns: the unit group or None if it cannot be determined."""
    if agg_type and agg_type in agg_group:
        return agg_group[agg_type]
    else:
        return obs_group_dict.get(obs_type)
    
def convert(val_t, target_unit_type):
    """ Convert a value or a sequence of values between unit systems

    val_t: A value-tuple with the value to be converted. The first
    element is the value (either a scalar or iterable), the second element 
    the unit type (e.g., "foot", or "inHg") it is in.
    
    target_unit_type: The unit type (e.g., "meter", or "mbar") to
    which the value is to be converted. 
    
    returns: An instance of ValueTuple, converted into the desired units.
    """
    # If the value is already in the target unit type, then just return it:
    if val_t[1] == target_unit_type:
        return val_t

    # Retrieve the conversion function. An exception of type KeyError
    # will occur if the target or source units are invalid
    try:
        conversion_func = conversionDict[val_t[1]][target_unit_type]
    except KeyError:
        log.debug("Unable to convert from %s to %s", val_t[1], target_unit_type)
        raise
    # Try converting a sequence first. A TypeError exception will occur if
    # the value is actually a scalar:
    try:
        new_val = list([conversion_func(x) if x is not None else None for x in val_t[0]])
    except TypeError:
        new_val = conversion_func(val_t[0]) if val_t[0] is not None else None
    # Add on the unit type and the group type and return the results:
    return ValueTuple(new_val, target_unit_type, val_t[2])

def convertStd(val_t, target_std_unit_system):
    """Convert a value tuple to an appropriate unit in a target standardized
    unit system
    
    val_t: A value tuple.
    
    target_std_unit_system: A standardized unit system
                            (weewx.US, weewx.METRIC, or weewx.METRICWX)
    
    Returns: A value tuple in the given standardized unit system.
    
    Example:
    >>> value_t = (30.02, 'inHg', 'group_pressure')
    >>> print("(%.2f, %s, %s)" % convertStd(value_t, weewx.METRIC))
    (1016.59, mbar, group_pressure)
    >>> value_t = (1.2, 'inch', 'group_rain')
    >>> print("(%.2f, %s, %s)" % convertStd(value_t, weewx.METRICWX))
    (30.48, mm, group_rain)
    """
    return StdUnitConverters[target_std_unit_system].convert(val_t)

def getStandardUnitType(target_std_unit_system, obs_type, agg_type=None):
    """Given a standard unit system (weewx.US, weewx.METRIC, weewx.METRICWX),
    an observation type, and an aggregation type, what units would it be in?
    
    target_std_unit_system: A standardized unit system. If None, then
    the the output units are indeterminate, so (None, None) is returned. 
    
    obs_type: An observation type.
        
    agg_type: An aggregation type E.g., 'mintime', or 'avg'.
    
    returns: A 2-way tuple containing the target units, and the target group.

    Examples:
    >>> print(getStandardUnitType(weewx.US,     'barometer'))
    ('inHg', 'group_pressure')
    >>> print(getStandardUnitType(weewx.METRIC, 'barometer'))
    ('mbar', 'group_pressure')
    >>> print(getStandardUnitType(weewx.US, 'barometer', 'mintime'))
    ('unix_epoch', 'group_time')
    >>> print(getStandardUnitType(weewx.METRIC, 'barometer', 'avg'))
    ('mbar', 'group_pressure')
    >>> print(getStandardUnitType(weewx.METRIC, 'wind', 'rms'))
    ('km_per_hour', 'group_speed')
    >>> print(getStandardUnitType(None, 'barometer', 'avg'))
    (None, None)
    """
    
    if target_std_unit_system is not None:
        return StdUnitConverters[target_std_unit_system].getTargetUnit(obs_type, agg_type)
    else:
        return (None, None)

def get_format_string(formatter, converter, obs_type):
    # First convert to the target unit type:
    u = converter.getTargetUnit(obs_type)[0]
    # Then look up the format string for that unit type:
    return formatter.get_format_string(u)

def get_label_string(formatter, converter, obs_type, plural=True):
    # First convert to the target unit type:    
    u = converter.getTargetUnit(obs_type)[0]
    # Then look up the label for that unit type:
    return formatter.get_label_string(u, plural)

class GenWithConvert(object):
    """Generator wrapper. Converts the output of the wrapped generator to a
    target unit system.
    
    Example:
    >>> def genfunc():
    ...    for i in range(3):
    ...        _rec = {'dateTime' : 194758100 + i*300,
    ...            'outTemp' : 68.0 + i * 9.0/5.0,
    ...            'usUnits' : weewx.US}
    ...        yield _rec
    >>> # First, try the raw generator function. Output should be in US
    >>> for _out in genfunc():
    ...    print("Timestamp: %d; Temperature: %.2f; Unit system: %d" % (_out['dateTime'], _out['outTemp'], _out['usUnits']))
    Timestamp: 194758100; Temperature: 68.00; Unit system: 1
    Timestamp: 194758400; Temperature: 69.80; Unit system: 1
    Timestamp: 194758700; Temperature: 71.60; Unit system: 1
    >>> # Now do it again, but with the generator function wrapped by GenWithConvert:
    >>> for _out in GenWithConvert(genfunc(), weewx.METRIC):
    ...    print("Timestamp: %d; Temperature: %.2f; Unit system: %d" % (_out['dateTime'], _out['outTemp'], _out['usUnits']))
    Timestamp: 194758100; Temperature: 20.00; Unit system: 16
    Timestamp: 194758400; Temperature: 21.00; Unit system: 16
    Timestamp: 194758700; Temperature: 22.00; Unit system: 16
    """
    
    def __init__(self, input_generator, target_unit_system=weewx.METRIC):
        """Initialize an instance of GenWithConvert
        
        input_generator: An iterator which will return dictionary records.
        
        target_unit_system: The unit system the output of the generator should
        use, or 'None' if it should leave the output unchanged."""
        self.input_generator = input_generator
        self.target_unit_system = target_unit_system
        
    def __iter__(self):
        return self
    
    def __next__(self):
        _record = next(self.input_generator)
        if self.target_unit_system is None:
            return _record
        else:
            return to_std_system(_record, self.target_unit_system)

    # For Python 2:
    next = __next__


def to_US(datadict):
    """Convert the units used in a dictionary to US Customary."""
    return to_std_system(datadict, weewx.US)

def to_METRIC(datadict):
    """Convert the units used in a dictionary to Metric."""
    return to_std_system(datadict, weewx.METRIC)

def to_METRICWX(datadict):
    """Convert the units used in a dictionary to MetricWX."""
    return to_std_system(datadict, weewx.METRICWX)

def to_std_system(datadict, unit_system):
    """Convert the units used in a dictionary to a target unit system."""
    if datadict['usUnits'] == unit_system:
        # It's already in the unit system.
        return datadict
    else:
        # It's in something else. Perform the conversion
        _datadict_target = StdUnitConverters[unit_system].convertDict(datadict)
        # Add the new unit system
        _datadict_target['usUnits'] = unit_system
        return _datadict_target


def as_value_tuple(record_dict, obs_type):
    """Look up an observation type in a record, returning the result
    as a ValueTuple. If the observation type is not recognized, an object of
    type UnknownType will be returned."""

    # Is the record None?
    if record_dict is None:
        # Yes. Signal a value of None and, arbitrarily, pick the US unit system:
        val = None
        std_unit_system = weewx.US
    else:
        # There is a record. Get the value, and the unit system.
        val = record_dict.get(obs_type)
        std_unit_system = record_dict['usUnits']

    # Given this standard unit system, what is the unit type of this
    # particular observation type? If the observation type is not recognized,
    # a unit_type of None will be returned
    (unit_type, unit_group) = StdUnitConverters[std_unit_system].getTargetUnit(obs_type)

    # Form the value-tuple and return it:
    return ValueTuple(val, unit_type, unit_group)


if __name__ == "__main__":
    
    import doctest

    if not doctest.testmod().failed:
        print("PASSED")

Reply via email to