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")